TMNL codebase conventions for file organization, barrel exports, naming patterns, comments, and module structure. The meta-skill for consistency.
TMNL maintains strict conventions for code organization, naming, and documentation. This skill codifies patterns observed across the codebase to ensure consistency.
Mandatory Conventions (Non-negotiable):
When: Creating a new library at src/lib/{feature}/
src/lib/{feature}/
āāā index.ts # Barrel export (required)
āāā types.ts # TypeScript types (UI props, configs)
āāā schemas/ # Effect Schema definitions
ā āāā index.ts
āāā services/ # Effect.Service implementations
ā āāā index.ts
ā āāā {Feature}Service.ts
āāā atoms/ # effect-atom definitions
ā āāā index.ts
āāā hooks/ # React hooks
ā āāā use{Feature}.ts
āāā components/ # React components (if UI-heavy)
ā āāā {Component}.tsx
āāā machines/ # XState machines (if stateful)
ā āāā {feature}-machine.ts
āāā v1/, v2/ # Version directories (for evolution)
āāā CLAUDE.{feature}.md # Agent handoff documentation
| Directory | When to Create |
|---|---|
services/ |
Multiple Effect services, or single complex service |
service.ts (root) |
Single simple service (e.g., src/lib/commands/service.ts) |
atoms/ |
effect-atom state management |
hooks/ |
Multiple React hooks |
use{Feature}.tsx (root) |
Single hook |
schemas/ |
Domain types requiring validation |
types.ts |
UI props, configs, non-validated types |
machines/ |
XState state machines |
v1/, v2/ |
Major version evolution (not breaking changes) |
When: Every src/lib/{feature}/ directory requires a barrel export.
/**
* {Feature} Library
*
* {Brief description}
*
* @example
* ```tsx
* import { Component, useFeature } from '@/lib/{feature}'
* ```
*/
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// CORE EXPORTS
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export { MainComponent } from './components/Main'
export { FeatureService } from './services'
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// SCHEMAS
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export * from './schemas'
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// ATOMS (effect-atom)
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export {
stateAtom,
configAtom,
operationsAtom,
} from './atoms'
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// REACT HOOKS
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export { useFeature, useFeatureState } from './hooks'
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// TYPES
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export type { FeatureConfig, FeatureState } from './types'
Use box-drawing characters for major sections:
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// SECTION NAME (all caps)
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Use simple dashes for subsections:
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// Subsection name
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
For modules with v1/v2:
// Default exports from v1
export * from './v1'
// Explicit v2 namespace
export * as v2 from './v2'
Usage:
import { Slider } from '@/lib/slider' // v1 (default)
import { Slider } from '@/lib/slider/v2' // v2 (explicit)
| Pattern | Example | Usage |
|---|---|---|
{noun}Atom |
resultsAtom |
State atom |
{noun}sAtom |
layersAtom |
Collection atom |
{verb}Atom |
computedAtom |
Derived/computed atom |
{feature}OpsAtom |
layerOpsAtom |
Operation atoms (mutations) |
{verb}Op |
searchOp, clearOp |
Individual operation |
| Pattern | Example | Usage |
|---|---|---|
{Feature}Service |
DataManagerService |
Effect.Service class |
{Feature}ServiceShape |
ChannelServiceShape |
Interface type |
{Behavior}Behavior |
LinearBehavior, DecibelBehavior |
Strategy pattern |
| Pattern | Example | Usage |
|---|---|---|
use{Feature} |
useSlider |
Main feature hook |
use{Feature}Value |
useSliderValue |
Read-only value hook |
use{Feature}State |
useMinibufferState |
State + updater |
use{Feature}Ops |
useOverlayOps |
Operations only |
useAtom{Feature} |
useAtomValue |
Atom-specific |
| Pattern | Example | Usage |
|---|---|---|
{Name}.tsx |
Slider.tsx |
Component file |
{Name}Props |
SliderProps |
Props interface |
{Name}Context |
DataGridContext |
Context type |
{Name}Provider |
CommandProvider |
Context provider |
| Pattern | Example | Usage |
|---|---|---|
{feature}Machine |
minibufferMachine |
XState machine |
{Feature}Machine |
MinibufferMachine |
Type alias |
{Feature}Actor |
MinibufferActor |
ActorRef type |
/**
* {Module Name} ā {Brief description}
*
* {Longer description if needed}
*
* @example
* ```tsx
* // Usage example
* ```
*/
For non-obvious design decisions:
// ARCHITECTURAL NOTE:
// CommandProvider lives here (commands/), not in minibuffer/.
// Minibuffer is a generic prompt engine. Commands USES minibuffer, not the reverse.
// TODO(prime): Migrate to v2 API after EPOCH-0003
// FIXME: This breaks when input is empty
// HACK: Workaround for WSLg rendering bug
types.ts// src/lib/slider/v1/types.ts
export interface SliderProps {
value: number
onChange: (value: number) => void
min?: number
max?: number
}
export interface SliderConfig {
min: number
max: number
step: number
defaultValue: number
}
schemas/// src/lib/overlays/schemas/core.ts
import { Schema } from 'effect'
export const OverlayId = Schema.String.pipe(
Schema.brand('OverlayId')
)
export type OverlayId = typeof OverlayId.Type
export const OverlayOpened = Schema.TaggedStruct('OverlayOpened', {
id: OverlayId,
timestamp: Schema.DateFromSelf,
})
| Pattern | Example | When |
|---|---|---|
__tests__/ directory |
src/lib/stx/__tests__/ |
Multiple test files |
.test.ts suffix |
service.test.ts |
Single test file |
.bun.test.ts suffix |
eventlog-integration.bun.test.ts |
Bun-specific |
// src/lib/feature/__tests__/feature.test.ts
describe('FeatureService', () => {
describe('operation', () => {
it('does X when Y', () => { ... })
it('fails with Z when W', () => { ... })
})
})
Use @effect/vitest with it.effect():
import { describe, it } from '@effect/vitest'
describe('MyService', () => {
it.effect('returns data', () =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* service.getData()
expect(result).toBeDefined()
}).pipe(Effect.provide(MyService.Default))
)
})
Use Registry.make():
it('atom updates', () => {
const r = Registry.make()
expect(r.get(counterAtom)).toBe(0)
r.set(counterAtom, 1)
expect(r.get(counterAtom)).toBe(1)
})
| File | Purpose | Audience |
|---|---|---|
CLAUDE.{feature}.md |
Agent handoff | AI assistants |
README.md |
User-facing docs | Developers |
ARCHITECTURE.md |
Deep design analysis | Architects |
AGENTS.{feature}.md |
Agent-specific notes | AI assistants |
# {Feature} ā Claude Context
## Overview
{Brief description}
## Key Files
- `service.ts` ā Main service implementation
- `atoms/index.ts` ā Reactive state
## Patterns Used
- Effect.Service<>() for DI
- Atom.runtime() for state
## Gotchas
- Don't use X because Y
- Always Z before W
## Related Skills
- effect-patterns
- effect-atom-integration
// src/lib/commands/service.ts
/**
* TMNL Commands ā Effect Service
*/
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// Atoms (Reactive State)
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export const commandsAtom = Atom.make<ReadonlyMap<string, Command>>(new Map())
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// Service Implementation
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export class CommandService extends Effect.Service<CommandService>()('app/CommandService', {
effect: Effect.gen(function* () {
// ...
}),
}) {}
src/lib/overlays/services/
āāā index.ts # Re-exports all services
āāā OverlayRegistry.ts # Registry service
āāā PortHub.ts # Port management
āāā EventDispatcher.ts # Event dispatch
// WRONG ā Unorganized exports
export * from './components'
export * from './hooks'
export * from './types'
export * from './atoms'
// CORRECT ā Sectioned exports
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// COMPONENTS
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export { Slider } from './components/Slider'
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// HOOKS
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
export { useSlider } from './hooks/useSlider'
// WRONG ā Domain type without Schema
// src/lib/feature/types.ts
export interface UserEvent {
_tag: 'UserCreated'
id: string
name: string
}
// CORRECT ā Domain type with Schema
// src/lib/feature/schemas/events.ts
export const UserCreated = Schema.TaggedStruct('UserCreated', {
id: Schema.String,
name: Schema.NonEmptyString,
})
// WRONG ā Mixed naming styles
export const user_state_atom = Atom.make(...) // snake_case
export const UseUserHook = () => { ... } // PascalCase for hook
export const userservice = Effect.Service() // no separator
// CORRECT ā Consistent camelCase with type suffix
export const userStateAtom = Atom.make(...)
export const useUser = () => { ... }
export const UserService = Effect.Service()
// WRONG ā No documentation
export const searchOp = runtimeAtom.fn<string>()(...)
// CORRECT ā JSDoc on public exports
/**
* Search operation. Triggers search with given query.
*
* @param query - Search query string
* @returns Effect that updates resultsAtom
*/
export const searchOp = runtimeAtom.fn<string>()(...)
When creating src/lib/{feature}/:
index.ts with JSDoc header and sectioned exportstypes.ts for non-domain types (UI props, configs)schemas/ for domain types using Effect Schemaservices/ (or service.ts) with Effect.Service<>()atoms/index.ts with Atom definitionshooks/ with React hooks// āāāāāāā...CLAUDE.{feature}.md for agent handoffAtom.make<T>() over Effect.Ref<T> for React@effect/vitest for services, Registry.make() for atoms| Convention | Best Example | File |
|---|---|---|
| Barrel file | Overlays | src/lib/overlays/index.ts |
| Service pattern | Commands | src/lib/commands/service.ts |
| Atom organization | Slider | src/lib/slider/v1/atoms/index.ts |
| Schema usage | Commands | src/lib/commands/types.ts |
| Hook naming | Commands | src/lib/commands/useCommandWire.tsx |
| Version strategy | Slider | src/lib/slider/index.ts |
| Test organization | STX | src/lib/stx/__tests__/ |