Core library for Kaiord workout data conversion
npm install @kaiord/core


Core library for Kaiord workout data conversion. Contains domain types, schemas, ports, use cases, and the plugin architecture for format adapters.
> Note: As of v2.0, format adapters (FIT, TCX, ZWO) are in separate packages. Install only the adapters you need for optimal bundle size.
- KRD canonical format with Zod schema validation
- Hexagonal architecture with ports and adapters pattern
- Plugin architecture for format adapters
- Full TypeScript support with type inference
- Custom logger injection
- Tree-shakeable ESM bundle
``bash`
npm install @kaiord/core
or with pnpm:
`bash`
pnpm add @kaiord/core
or with yarn:
`bash`
yarn add @kaiord/core
`typescript
import { createDefaultProviders } from "@kaiord/core";
import { createFitProviders } from "@kaiord/fit";
import type { KRD } from "@kaiord/core";
import { readFile } from "fs/promises";
// Wire FIT adapter into core
const providers = createDefaultProviders({
fit: createFitProviders(),
});
// Convert FIT to KRD
const fitBuffer = await readFile("workout.fit");
const krd: KRD = await providers.convertFitToKrd!({ fitBuffer });
// Convert KRD to FIT
const output = await providers.convertKrdToFit!({ krd });
`
`typescript
import { createDefaultProviders } from "@kaiord/core";
import { createFitProviders } from "@kaiord/fit";
import { createTcxProviders } from "@kaiord/tcx";
import { createZwoProviders } from "@kaiord/zwo";
// Wire all adapters you need
const providers = createDefaultProviders({
fit: createFitProviders(),
tcx: createTcxProviders(),
zwo: createZwoProviders(),
});
// Convert between formats
const krd = await providers.convertFitToKrd!({ fitBuffer });
const tcx = await providers.convertKrdToTcx!({ krd });
const zwo = await providers.convertKrdToZwift!({ krd });
`
`typescript
import { krdSchema } from "@kaiord/core";
// Validate KRD data
const result = krdSchema.safeParse(data);
if (result.success) {
console.log("Valid KRD:", result.data);
} else {
console.error("Validation errors:", result.error.errors);
}
`
#### createDefaultProviders(adapters?, logger?)
Creates a provider container with optional format adapters wired in.
`typescript
import { createDefaultProviders } from "@kaiord/core";
import { createFitProviders } from "@kaiord/fit";
import { createTcxProviders } from "@kaiord/tcx";
const providers = createDefaultProviders({
fit: createFitProviders(),
tcx: createTcxProviders(),
});
// Returns: { schemaValidator, toleranceChecker, logger, convertFitToKrd, ... }
`
#### convertFitToKrd({ fitBuffer })
Converts a FIT workout file to KRD format.
`typescript`
const krd = await providers.convertFitToKrd({ fitBuffer });
Parameters:
- fitBuffer: Uint8Array - Binary FIT file data
Returns: Promise - Validated KRD object
Throws:
- FitParsingError - When FIT file is corrupted or invalidKrdValidationError
- - When converted data fails schema validation
#### convertKrdToFit({ krd })
Converts a KRD object to FIT workout file format.
`typescript`
const fitBuffer = await providers.convertKrdToFit({ krd });
Parameters:
- krd: KRD - Valid KRD object
Returns: Promise - Binary FIT file data
Throws:
- KrdValidationError - When KRD data is invalidFitParsingError
- - When FIT encoding fails
#### validateRoundTrip({ fitBuffer })
Validates that FIT → KRD → FIT conversion preserves data within tolerances.
`typescript
import { validateRoundTrip, createToleranceChecker } from "@kaiord/core";
const checker = createToleranceChecker();
await validateRoundTrip(
fitReader,
fitWriter,
validator,
checker,
logger
)({
fitBuffer,
});
`
Throws:
- ToleranceExceededError - When round-trip conversion exceeds tolerances
All domain schemas are exported for validation and type inference:
`typescript
import {
krdSchema,
workoutSchema,
durationSchema,
targetSchema,
sportSchema,
subSportSchema,
intensitySchema,
} from "@kaiord/core";
// Validate data
const result = krdSchema.safeParse(data);
// Access enum values
const sport = sportSchema.enum.cycling;
const intensity = intensitySchema.enum.warmup;
`
All TypeScript types are inferred from Zod schemas:
`typescript`
import type {
KRD,
Workout,
WorkoutStep,
Duration,
Target,
Sport,
SubSport,
Intensity,
} from "@kaiord/core";
`typescript`
import {
FitParsingError,
KrdValidationError,
ToleranceExceededError,
} from "@kaiord/core";
For detailed API examples, see docs/api-examples.md (coming soon).
@kaiord/core is written in TypeScript and provides complete type definitions.
Import types separately from values for optimal tree-shaking:
`typescript`
import { createDefaultProviders, krdSchema } from "@kaiord/core";
import type { KRD, Workout, Duration } from "@kaiord/core";
Duration and Target types use discriminated unions for type safety:
`typescript
import type { Duration } from "@kaiord/core";
const duration: Duration =
| { type: "time"; seconds: number }
| { type: "distance"; meters: number }
| { type: "open" };
// TypeScript narrows the type based on discriminator
if (duration.type === "time") {
console.log(duration.seconds); // ✓ TypeScript knows this exists
}
`
`typescript
import type { Target } from "@kaiord/core";
const target: Target =
| { type: "power"; value: { unit: "watts"; value: number } }
| { type: "heart_rate"; value: { unit: "bpm"; value: number } }
| { type: "open" };
// Type narrowing works automatically
if (target.type === "power") {
console.log(target.value.unit); // ✓ "watts" | "percent_ftp" | "zone" | "range"
}
`
Zod schemas provide both runtime validation and TypeScript types:
`typescript
import { krdSchema, workoutSchema } from "@kaiord/core";
import type { KRD, Workout } from "@kaiord/core";
// Parse with automatic type inference
const krd = krdSchema.parse(data); // Type: KRD
// Safe parse with error handling
const result = krdSchema.safeParse(data);
if (result.success) {
const krd: KRD = result.data; // Type: KRD
} else {
console.error(result.error.errors);
}
// Validate nested objects
const workout = workoutSchema.parse(data); // Type: Workout
`
Access enum values via schema .enum property:
`typescript
import { sportSchema, intensitySchema } from "@kaiord/core";
// Access enum values
const sport = sportSchema.enum.cycling; // "cycling"
const intensity = intensitySchema.enum.warmup; // "warmup"
// Use in comparisons
if (workout.sport === sportSchema.enum.running) {
console.log("Running workout");
}
`
@kaiord/core uses custom error classes for different failure scenarios.
#### FitParsingError
Thrown when FIT file parsing fails due to corrupted or invalid data.
`typescript
import { FitParsingError } from "@kaiord/core";
try {
const krd = await providers.convertFitToKrd({ fitBuffer });
} catch (error) {
if (error instanceof FitParsingError) {
console.error("Failed to parse FIT file:", error.message);
console.error("Original error:", error.cause);
}
}
`
Properties:
- message: string - Error descriptioncause?: unknown
- - Original error from FIT SDK
#### KrdValidationError
Thrown when KRD data fails schema validation.
`typescript
import { KrdValidationError } from "@kaiord/core";
try {
const krd = await providers.convertFitToKrd({ fitBuffer });
} catch (error) {
if (error instanceof KrdValidationError) {
console.error("KRD validation failed:");
for (const err of error.errors) {
console.error( - ${err.field}: ${err.message});`
}
}
}
Properties:
- message: string - Error descriptionerrors: Array<{ field: string; message: string }>
- - Validation errors
#### ToleranceExceededError
Thrown when round-trip conversion exceeds defined tolerances.
`typescript
import { ToleranceExceededError } from "@kaiord/core";
try {
await validateRoundTrip(
fitReader,
fitWriter,
validator,
checker,
logger
)({
fitBuffer,
});
} catch (error) {
if (error instanceof ToleranceExceededError) {
console.error("Round-trip validation failed:");
for (const violation of error.violations) {
console.error(
- ${violation.field}: expected ${violation.expected}, got ${violation.actual} Deviation: ${violation.deviation}, tolerance: ${violation.tolerance}
);
console.error(
`
);
}
}
}
Properties:
- message: string - Error descriptionviolations: Array<{ field: string; expected: number; actual: number; deviation: number; tolerance: number }>
- - Tolerance violations
`typescript
import {
createDefaultProviders,
FitParsingError,
KrdValidationError,
ToleranceExceededError,
} from "@kaiord/core";
async function convertWorkout(fitBuffer: Uint8Array) {
try {
const providers = createDefaultProviders();
const krd = await providers.convertFitToKrd({ fitBuffer });
return krd;
} catch (error) {
if (error instanceof FitParsingError) {
console.error("❌ FIT parsing failed:", error.message);
if (error.cause) {
console.error(" Cause:", error.cause);
}
throw new Error("Invalid FIT file");
}
if (error instanceof KrdValidationError) {
console.error("❌ KRD validation failed:");
for (const err of error.errors) {
console.error( - ${err.field}: ${err.message});
}
throw new Error("Conversion produced invalid KRD");
}
// Unknown error - re-throw
throw error;
}
}
`
- Getting Started - Quick start guide and installation
- Architecture - Hexagonal architecture, ports & adapters pattern
- Testing Guidelines - Testing patterns and best practices
- KRD Format Specification - Complete format documentation
- API Examples - Comprehensive code examples (coming soon)
- Tree Shaking Guide - Optimize bundle size
- KRD Fixtures Generation - Generate test fixtures
- Zwift Format Extensions - Zwift-specific features
- Zwift Kaiord Attributes - Custom Zwift attributes
We welcome contributions! Please see:
- Contributing Guidelines - How to contribute to Kaiord
- Issue Tracker - Report bugs or request features
- Pull Requests - Submit code changes
Before contributing:
1. Read the Architecture documentation
2. Follow the Testing guidelines
3. Ensure all tests pass: pnpm testpnpm lint
4. Run linter:
`bash`
pnpm build # Build the library
pnpm test # Run tests once
pnpm test:watch # Run tests in watch mode
pnpm test:coverage # Run tests with coverage report
pnpm generate:schema # Generate JSON Schema from Zod schemas
pnpm generate:krd-fixtures # Generate KRD test fixtures from FIT files
pnpm clean # Clean build artifacts
@kaiord/core is fully optimized for tree-shaking. Import only what you need:
`typescript
// ✅ Good: Import specific items (smaller bundle)
import { krdSchema, sportSchema } from "@kaiord/core";
import type { KRD, Sport } from "@kaiord/core";
// Test utilities (separate export, not included in main bundle)
import { loadKrdFixture } from "@kaiord/core/test-utils";
// ❌ Avoid: Import everything (larger bundle)
import * as Kaiord from "@kaiord/core";
`
Bundle sizes (minified + gzipped):
- Types only: 0 KB (compile-time)
- Schema validation: ~15 KB
- Full conversion: ~80 KB
- Test utilities: Not included in production bundles
See docs/tree-shaking.md for detailed guide and best practices.
The package exports test utilities for other packages to use:
`typescript
import {
loadFitFixture,
loadKrdFixture,
loadFixturePair,
FIXTURE_NAMES,
} from "@kaiord/core/test-utils";
// Load fixtures for testing
const fitBuffer = loadFitFixture("WorkoutIndividualSteps.fit");
const krd = loadKrdFixture("WorkoutIndividualSteps.krd");
// Load both for round-trip tests
const { fit, krd } = loadFixturePair(FIXTURE_NAMES.INDIVIDUAL_STEPS);
``
See docs/krd-fixtures-generation.md for details on fixture generation.