Core Package Skill
Core Principles
- ZERO Database Dependencies - No Drizzle, Prisma, Supabase, or ORM imports
- Pure Functions Only - No async, no side effects, deterministic
- Zod for Validation - Runtime type safety at boundaries
- Type Inference - Use
z.infer<>for single source of truth - Comprehensive Testing - 100% coverage for calculations
- Self-Documenting - Clear names + JSDoc for public APIs
Patterns to Follow
Pattern 1: Pure Function with Parameter Object
export interface TSSParams {
normalizedPower: number;
duration: number; // seconds
ftp: number;
}
export function calculateTSS(params: TSSParams): number {
const { normalizedPower, duration, ftp } = params;
if (!ftp || ftp === 0) return 0;
if (!duration || duration === 0) return 0;
const intensityFactor = normalizedPower / ftp;
const hours = duration / 3600;
return ((duration * normalizedPower * intensityFactor) / (ftp * 3600)) * 100;
}
Key Points:
- Parameter object for 3+ params
- Guard clauses for edge cases
- No mutations, no side effects
- Return value only
Pattern 2: Zod Schema with Type Inference
export const activitySchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, "Name is required"),
type: z.enum(["run", "bike", "swim", "other"]),
distance: z.number().nonnegative().optional(),
duration: z.number().int().positive(),
});
export type Activity = z.infer<typeof activitySchema>;
Key Points:
- Schema first, type second
- Custom error messages
- Optional fields explicit
- Single source of truth
Pattern 3: Discriminated Union with Zod
const durationTimeSchema = z.object({
type: z.literal("time"),
seconds: z.number().int().positive(),
});
const durationDistanceSchema = z.object({
type: z.literal("distance"),
meters: z.number().positive(),
});
export const durationSchema = z.discriminatedUnion("type", [
durationTimeSchema,
durationDistanceSchema,
]);
export type Duration = z.infer<typeof durationSchema>;
// Usage with type narrowing
function formatDuration(duration: Duration): string {
if (duration.type === "time") {
return `${duration.seconds}s`; // TypeScript knows seconds exists
} else {
return `${duration.meters}m`; // TypeScript knows meters exists
}
}
Pattern 4: JSDoc for Public APIs
/**
* Calculates Training Stress Score (TSS) from power data.
*
* TSS quantifies training load based on:
* - Normalized Power (30-second rolling average)
* - Intensity Factor (NP / FTP)
* - Duration
*
* Formula: TSS = (duration × NP × IF) / (FTP × 3600) × 100
*
* @param params - Power stream, timestamps, and FTP
* @returns TSS value (0-300+ typical range)
*
* @example
* ```typescript
* const tss = calculateTSS({
* normalizedPower: 250,
* duration: 3600,
* ftp: 250,
* });
* console.log(tss); // 100
* ```
*/
export function calculateTSS(params: TSSParams): number {
// Implementation
}
Pattern 5: Comprehensive Testing
describe("calculateTSS", () => {
it("should calculate TSS correctly for 1 hour at FTP", () => {
const result = calculateTSS({
normalizedPower: 250,
duration: 3600,
ftp: 250,
});
expect(result).toBe(100);
});
it("should return 0 for zero FTP", () => {
const result = calculateTSS({
normalizedPower: 250,
duration: 3600,
ftp: 0,
});
expect(result).toBe(0);
});
it("should handle zero duration", () => {
const result = calculateTSS({
normalizedPower: 250,
duration: 0,
ftp: 250,
});
expect(result).toBe(0);
});
});
Anti-Patterns to Avoid
Anti-Pattern 1: Database Imports
// ❌ WRONG
import { db } from '@repo/supabase';
export async function calculateUserTSS(userId: string) {
const activities = await db.activities.findMany({ userId });
return activities.reduce((sum, act) => sum + act.tss, 0);
}
// ✅ CORRECT - Move to tRPC layer
// In packages/trpc/src/routers/activities.ts
export const activityRouter = router({
getUserTSS: protectedProcedure.query(async ({ ctx }) => {
const activities = await ctx.db.activities.findMany({...});
return activities.reduce((sum, act) => sum + act.tss, 0);
}),
});
Anti-Pattern 2: Async Operations
// ❌ WRONG
export async function calculateTSS(...) {
const ftp = await getFTP(); // NO!
}
// ✅ CORRECT
export function calculateTSS(params: { ftp: number, ... }): number {
// Pure, synchronous
}
Anti-Pattern 3: Side Effects
// ❌ WRONG
let cache = {};
export function calculateTSS(...) {
cache[key] = value; // MUTATION!
}
// ✅ CORRECT
export function calculateTSS(...): number {
return value; // No mutations
}
File Organization
packages/core/
├── calculations/
│ ├── tss.ts
│ ├── tss.test.ts
│ └── zones.ts
├── schemas/
│ ├── activity.ts
│ ├── activity.test.ts
│ └── profile.ts
├── utils/
│ ├── time.ts
│ └── distance.ts
└── index.ts
Dependencies
Allowed:
zod- Validation- Pure utility libraries (no I/O)
Forbidden:
@supabase/*drizzle-orm,prisma@repo/trpcreact,react-native
Checklist
- No database imports
- No async functions
- Pure functions only
- Zod schemas for validation
- Type inference from schemas
- JSDoc on public APIs
- 100% test coverage
- Named exports only
Related Skills
- Backend Skill - Orchestration layer
- Testing Skill - Pure function testing
Version History
- 1.0.0 (2026-01-21): Initial version
Next Review: 2026-02-21