This document defines the coding standards for the Oshun monorepo. These standards are enforced through ESLint, Prettier, TypeScript compiler options, and pre-commit hooks.
Table of Contents#
- TypeScript Configuration
- ESLint Rules
- Prettier Formatting
- File Organization
- Naming Conventions
- Import Organization
- Error Handling Patterns
- Testing Patterns
- Domain-Specific Rules
TypeScript Configuration#
The root tsconfig.base.json defines the baseline TypeScript configuration. All
projects extend from it.
Compiler Options#
| Option | Value | Purpose |
|---|---|---|
target |
ES2022 |
Modern JavaScript output |
module |
ESNext |
ESM modules |
moduleResolution |
bundler |
Bundler-compatible resolution |
strict |
true |
All strict checks enabled |
noImplicitOverride |
true |
Requires override keyword |
noImplicitReturns |
true |
All code paths must return |
noFallthroughCasesInSwitch |
true |
Switch cases must break/return |
noUnusedLocals |
true |
No unused local variables |
noUnusedParameters |
true |
No unused function parameters |
isolatedModules |
true |
Required for esbuild/swc compatibility |
verbatimModuleSyntax |
true |
Enforces explicit type imports |
forceConsistentCasingInFileNames |
true |
Path casing must match filesystem |
esModuleInterop |
true |
CommonJS interop support |
skipLibCheck |
true |
Skip checking .d.ts files |
resolveJsonModule |
true |
Allow importing .json files |
experimentalDecorators |
true |
Decorator support |
emitDecoratorMetadata |
true |
Decorator metadata emission |
sourceMap |
true |
Source maps for debugging |
useDefineForClassFields |
true |
TC39 class field semantics |
Key Implications#
strict: true enables all of these checks:
strictNullChecks--nullandundefinedare distinct typesstrictFunctionTypes-- Contravariant function parameter checkingstrictBindCallApply-- Strictbind,call,applytypingstrictPropertyInitialization-- Class properties must be initializednoImplicitAny-- No implicitanytypesnoImplicitThis-- No implicitthistypesalwaysStrict-- Emit"use strict"in output
verbatimModuleSyntax: true requires explicit type annotations on imports:
// Correct -- type import is explicit
import { type SomeType, someFunction } from './module.js';
// Correct -- all types
import type { SomeType, AnotherType } from './module.js';
// Incorrect -- will error because SomeType is only used as a type
import { SomeType, someFunction } from './module.js';
isolatedModules: true means each file is compiled independently. This
prohibits:
const enum(use regularenumor union types instead)- Re-exporting types without
typekeyword - Namespace declarations that merge with values
Module System#
The monorepo uses ESM ("type": "module" in root package.json). All imports
must use ESM syntax:
// Correct
import { something } from './module.js';
export { something };
// Incorrect
const something = require('./module');
module.exports = something;
Path Aliases#
Path aliases are defined in tsconfig.base.json. The convention is:
- Shared libraries:
@oshun/{lib}maps tolibs/shared/{lib}/src/index.ts - Domain libraries:
@{domain}/{lib}maps tolibs/{domain}/{lib}/src/index.ts
Examples:
import { sql } from '@oshun/database';
import { AuthService } from '@oshun/auth';
import type { ConversationConfig } from '@iris/conversation-core';
ESLint Rules#
The root eslint.config.js uses the flat config format with
typescript-eslint.
Global Rules (All TypeScript Files)#
Type Safety#
| Rule | Level | Notes |
|---|---|---|
@typescript-eslint/no-explicit-any |
warn |
Prefer typed alternatives. Use unknown for truly unknown types. |
@typescript-eslint/no-unused-vars |
error |
Prefix with _ for intentionally unused: _unused, _err. Applies to args, vars, and caught errors. |
@typescript-eslint/no-non-null-assertion |
warn |
Avoid value!. Use type narrowing or optional chaining instead. |
@typescript-eslint/prefer-nullish-coalescing |
warn |
Use value ?? default instead of value || default. |
@typescript-eslint/prefer-optional-chain |
warn |
Use obj?.prop?.method() instead of obj && obj.prop && obj.prop.method(). |
@typescript-eslint/explicit-function-return-type |
off |
Return types are inferred. Add them for public API clarity if desired. |
@typescript-eslint/explicit-module-boundary-types |
off |
Module boundary return types are not required globally. |
Best Practices#
| Rule | Level | Notes |
|---|---|---|
no-console |
warn |
Only console.warn and console.error are allowed. Use @oshun/logger for structured logging. |
no-debugger |
error |
Must be removed before committing. |
no-duplicate-imports |
error |
Combine value and type imports from the same module. |
eqeqeq |
error |
Always use === and !==. Exception: null comparisons (== null allowed for checking both null and undefined). |
prefer-const |
error |
Use const unless the variable is reassigned. |
no-var |
error |
Always use let or const. |
Context-Specific Relaxations#
Test files (*.spec.ts, *.test.ts, __tests__/**):
@typescript-eslint/no-explicit-any-- OFF@typescript-eslint/no-non-null-assertion-- OFFno-console-- OFF
Server/service files (**/server.ts, **/services/**):
no-console-- OFF
CLI applications (apps/**/cli/**):
no-console-- OFF
Config files (*.config.js, *.config.ts):
@typescript-eslint/no-require-imports-- OFF
Legacy code (lilith/**, yemaya/**):
@nx/enforce-module-boundaries-- OFF (disabled during migration)@typescript-eslint/no-explicit-any--warn(relaxed)@typescript-eslint/no-require-imports-- OFF
Module Boundary Rules#
The @nx/enforce-module-boundaries rule enforces domain isolation. Every
project must have scope, layer, and type tags in its project.json.
Scope tags define domain ownership:
scope:shared, scope:contracts, scope:auth,
scope:lilith, scope:yemaya, scope:isis, scope:iris,
scope:sophia, scope:hathor, scope:bellona, scope:aphrodite,
scope:nyx, scope:tara, scope:veritas, scope:psyche, ...
Layer tags define architectural layer:
layer:contracts -> layer:infra -> layer:data -> layer:domain -> layer:ui
Type tags define project kind:
type:app, type:lib, type:util, type:e2e
Constraints:
type:appcan depend ontype:libandtype:utiltype:libcan depend ontype:libandtype:utiltype:e2ecan depend ontype:lib,type:util, andtype:app
Prettier Formatting#
Prettier is the sole code formatter. It runs as part of the pre-commit hook via
lint-staged and is checked in CI via pnpm format:check.
Configuration#
Prettier uses its default settings with the prettier-plugin-tailwindcss plugin
for Tailwind CSS class sorting. No custom .prettierrc file exists at the root
-- Prettier defaults apply.
Key defaults:
| Setting | Value |
|---|---|
| Print width | 80 |
| Tab width | 2 |
| Tabs | No (spaces) |
| Semicolons | Yes |
| Quotes | Double |
| Trailing comma | all |
| Bracket spacing | Yes |
| Arrow parens | always |
| End of line | lf |
File Types Formatted#
Prettier formats these file types (configured in lint-staged):
.js,.jsx,.ts,.tsx-- Also linted with ESLint first.json.md.yaml,.yml
Running Prettier#
# Format all files
pnpm format
# Check formatting (CI mode)
pnpm format:check
File Organization#
Library Structure#
Every library follows this standard structure:
libs/{domain}/{library-name}/
src/
index.ts # Public API barrel file
types.ts # Type definitions
{module}.ts # Implementation files
__tests__/ # Test directory (alternative to co-located tests)
{module}.spec.ts
project.json # Nx project configuration
package.json # Package metadata and dependencies
tsconfig.json # Extends ../../tsconfig.base.json (or ../../../)
tsconfig.lib.json # Library-specific TS config
tsconfig.spec.json # Test-specific TS config
vitest.config.ts # Vitest configuration
Public API (index.ts)#
The index.ts barrel file defines the public API of the library. It should
export all public types, functions, and classes:
/**
* @{scope}/{library-name}
*
* Description of what this library provides.
*
* @packageDocumentation
*/
// Types
export type { TypeA, TypeB, TypeC } from './types.js';
// Constants and defaults
export { DEFAULT_CONFIG, SOME_CONSTANT } from './types.js';
// Implementation
export { ServiceClass, createService } from './service.js';
export { helperFunction } from './helpers.js';
Conventions:
- Group exports by category (types, constants, implementations)
- Use section comments (
// ====...) to visually separate categories - Include JSDoc module documentation at the top
- Export types separately using
export type(required byverbatimModuleSyntax)
Application Structure#
Applications follow a domain-appropriate structure. Common patterns:
apps/{domain}/{app-name}/
src/
app.ts # Application entry point
routes/ # API route handlers
services/ # Business logic services
middleware/ # Request middleware
test/ # Test files
project.json
package.json
tsconfig.json
Project Configuration (project.json)#
Every project must include scope, layer, and type tags:
{
"name": "@domain/library-name",
"sourceRoot": "libs/domain/library-name/src",
"projectType": "library",
"tags": ["scope:domain", "layer:domain", "type:lib"],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"options": {
"outputPath": "dist/libs/domain/library-name",
"main": "libs/domain/library-name/src/index.ts",
"tsConfig": "libs/domain/library-name/tsconfig.lib.json"
}
},
"test": {
"executor": "@nx/vite:test",
"options": {
"config": "libs/domain/library-name/vitest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
Naming Conventions#
Files and Directories#
| Type | Convention | Example |
|---|---|---|
| Source files | kebab-case.ts |
auth-service.ts |
| Test files | *.spec.ts |
auth-service.spec.ts |
| Test files (alt) | *.test.ts |
auth-service.test.ts |
| Type definition files | types.ts |
types.ts |
| Index/barrel files | index.ts |
index.ts |
| Config files | *.config.ts |
vitest.config.ts |
| Directories | kebab-case |
auth-primitives/ |
| React components | PascalCase.tsx |
MeditationCard.tsx |
| CSS modules | *.module.css |
SafeArea.module.css |
Code Identifiers#
| Type | Convention | Example |
|---|---|---|
| Variables | camelCase |
const userName = 'john'; |
| Constants | UPPER_CASE or camelCase |
const MAX_RETRIES = 3; |
| Functions | camelCase |
function createService() {} |
| Classes | PascalCase |
class AuthService {} |
| Interfaces | PascalCase |
interface UserProfile {} |
| Type aliases | PascalCase |
type TokenPair = { ... }; |
| Enums | PascalCase |
enum UserRole { Admin, User } |
| Enum members | PascalCase |
UserRole.Admin |
| Generics | PascalCase |
function wrap<T>(value: T): T {} |
| Private fields | _camelCase |
private _connectionPool: Pool; |
| Unused params | _camelCase |
(_req, res) => {} |
Package Naming#
| Type | Pattern | Example |
|---|---|---|
| Shared libraries | @oshun/{lib-name} |
@oshun/database |
| Domain libraries | @{domain}/{lib-name} |
@iris/conversation-core |
| Applications | @{domain}/{app-name} |
@tara/api |
Interface Naming#
Prefer descriptive names without I prefix for most interfaces. Use I prefix
for repository/store interfaces that represent contracts:
// Repository contracts -- use I prefix
interface IUserRepository { ... }
interface ITokenRepository { ... }
interface IAuditRepository { ... }
// Data shape interfaces -- no I prefix
interface UserProfile { ... }
interface AuthResult { ... }
interface TokenPair { ... }
Import Organization#
Import Order#
Organize imports in this order, separated by blank lines:
- Node.js built-in modules (
node:fs,node:path, etc.) - External packages (
zod,hono,vitest, etc.) - Workspace packages (
@oshun/*,@iris/*, etc.) - Relative imports (
./module,../module)
import { readFile } from 'node:fs/promises';
import { z } from 'zod';
import { Hono } from 'hono';
import { sql, type ParameterizedQuery } from '@oshun/database';
import { AuthService } from '@oshun/auth';
import { processData } from './helpers.js';
import type { Config } from './types.js';
Import Rules#
-
No duplicate imports (
no-duplicate-imports: error). Combine value and type imports from the same module:ts// Correct import { sql, type ParameterizedQuery } from '@oshun/database'; // Incorrect -- separate imports from same module import { sql } from '@oshun/database'; import type { ParameterizedQuery } from '@oshun/database'; -
Use
typekeyword for type-only imports (required byverbatimModuleSyntax):ts// Type-only import import type { UserProfile } from './types.js'; // Mixed import with inline type import { AuthService, type AuthConfig } from '@oshun/auth'; -
Use
.jsextension for relative imports (required for ESM):ts// Correct import { helper } from './helpers.js'; // Incorrect import { helper } from './helpers';
Error Handling Patterns#
General Principles#
-
Never swallow errors silently. Always log or re-throw.
-
Use typed errors. The
@oshun/errorslibrary provides structured error types. Domain libraries define their own typed error classes:tsexport class AuthError extends Error { constructor( public code: AuthErrorCode, message: string, public details?: Record<string, unknown> ) { super(message); this.name = 'AuthError'; } } -
Use
unknownfor catch blocks (TypeScript strict mode requires this):tstry { await riskyOperation(); } catch (error: unknown) { if (error instanceof AuthError) { // Handle typed error } else { // Handle unknown error throw new Error('Unexpected error', { cause: error }); } } -
Prefix unused caught errors with
_:tstry { await operation(); } catch (_error) { // Intentionally ignoring the error return fallbackValue; }
Result Pattern#
For functions that can fail in expected ways, consider returning a result type instead of throwing:
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function authenticate(
credentials: LoginCredentials
): Promise<Result<AuthResult, AuthError>> {
// ...
}
Never Expose Internal Errors#
API error responses must never include stack traces, internal paths, or implementation details in production:
// Correct -- structured error response
return { error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } };
// Incorrect -- leaks internals
return { error: err.stack };
Testing Patterns#
Framework#
All tests use Vitest as the test runner, configured per-project via
vitest.config.ts.
Vitest Configuration#
Each project's vitest.config.ts must include path aliases that match
tsconfig.base.json:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
resolve: {
alias: {
'@oshun/database': '../database/src/index.ts',
'@oshun/errors': '../errors/src/index.ts',
},
},
});
Test File Naming#
- Unit tests:
*.spec.tsor*.test.ts-- co-located with source or in__tests__/directory - Integration tests:
*.integration.test.tsor intest/directory - E2E tests: In a dedicated
e2e/directory with Playwright
Test Structure#
Use descriptive describe and it blocks:
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
service = createAuthService(mockConfig);
});
describe('login', () => {
it('returns token pair for valid credentials', async () => {
const result = await service.login({
email: 'user@example.com',
password: 'ValidPass123!',
});
expect(result.success).toBe(true);
expect(result.data.accessToken).toBeDefined();
expect(result.data.refreshToken).toBeDefined();
});
it('returns error for invalid password', async () => {
const result = await service.login({
email: 'user@example.com',
password: 'wrong',
});
expect(result.success).toBe(false);
expect(result.error.code).toBe('INVALID_CREDENTIALS');
});
});
});
Assertion Patterns#
Common Vitest assertion patterns and their correct usage:
// Equality
expect(value).toBe(expected); // Strict equality (===)
expect(value).toEqual(expected); // Deep equality
expect(value).toBeCloseTo(3.14, 2); // Floating point (within 0.005)
// Array/object matching
expect(array).toContain(item); // Uses === (for primitives)
expect(array).toContainEqual(item); // Uses deep equality (for objects)
expect(obj).toMatchObject(partial); // Partial object match
// Truthiness
expect(value).toBeDefined();
expect(value).toBeUndefined();
expect(value).toBeTruthy();
expect(value).toBeFalsy();
// Async
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow('message');
Mocking#
Use Vitest's built-in mocking:
import { vi } from 'vitest';
// Mock a module
vi.mock('@oshun/database', () => ({
sql: vi.fn(),
createPool: vi.fn(),
}));
// Mock a function
const mockFn = vi.fn().mockReturnValue('result');
// Spy on a method
const spy = vi.spyOn(service, 'method');
// Reset mocks between tests
afterEach(() => {
vi.restoreAllMocks();
});
Domain-Specific Rules#
Uzume Domain (Live Performance & Stagecraft)#
The Uzume domain (libs/uzume/**) enforces stricter TypeScript rules due to its
real-time nature and entertainment protocol integrations:
| Rule | Level | Notes |
|---|---|---|
@typescript-eslint/no-explicit-any |
error |
No any allowed |
@typescript-eslint/explicit-function-return-type |
error |
All functions must have return types |
@typescript-eslint/explicit-module-boundary-types |
error |
All exported functions must have return types |
Naming conventions are strictly enforced:
- Default identifiers:
camelCase - Variables:
camelCase,UPPER_CASE, orPascalCase - Types/interfaces:
PascalCase - Enum members:
UPPER_CASEorPascalCase - Object literal and type properties: unrestricted (for protocol compatibility)
Entertainment protocol abbreviations are allowed in identifiers: DMX, OSC,
MIDI, SMPTE, GDTF, MVR, NDI, AES, LTC, MTC, RDM, ArtNet,
sACN
Browser Extension (Iris)#
Browser extension files (apps/iris/web/extension/**/*.js) have explicit
browser and WebExtension globals defined and relaxed rules for switch-case
patterns.
PWA and Widget (Iris)#
PWA files (apps/iris/web/pwa/**) and widget files (apps/iris/web/widget/**)
have no-console disabled and relaxed nullish coalescing rules for DOM
manipulation patterns.
Code Generation Libraries (Iris)#
Code generation and agentic libraries (libs/iris/code/generation/**,
libs/iris/code/agentic/**) have no-case-declarations disabled due to
extensive switch-case patterns.
Pre-commit Enforcement Summary#
Every commit is automatically checked by these hooks (via Husky):
-
Pre-commit (
lint-staged):eslint --fixon staged.js,.jsx,.ts,.tsxfilesprettier --writeon staged.js,.jsx,.ts,.tsx,.json,.md,.yaml,.ymlfiles
-
Commit-msg (
commitlint):- Validates the commit message against Conventional Commits format
- Enforces type, scope, subject, and length rules
If either hook fails, the commit is rejected. Fix the issues and try again.