This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Quick Reference
- Critical Rules
- Storage Solution
- Build/Test Commands
- Code Style Guidelines
- Best Practices
✅ MUST USE: knowledge-store - see Storage Solution
✅ MUST USE: promises(array) for dynamic arrays - see Concurrency Management
✅ MUST USE: instantOf/durationOf - see Time Representation
✅ MUST DO: Run builds from repository root with yarn build
✅ MUST DO: Update module's index.ts with new API declarations
❌ NEVER: directly edit the dependencies or the devDependencies field in package.json files. Instead use yarn deps:fix to adjust these based on the actual imports in the code.
⚠️ CRITICAL:knowledge-storeis the ONLY storage solution to use in this project.
⚠️ CRITICAL: Usepromises()with concurrency limits for arrays of unknown/dynamic length.Promise.all()can be used only for fixed-size arrays whose length is known at development time.
⚠️ CRITICAL: All new API declarations MUST be re-exported from the module'sindex.tsfile.
When working with ANY data storage requirements, you MUST use the @moojo/knowledge-store module. This is not optional or situational - it is MANDATORY for ALL data persistence needs in this project.
You MUST read modules/knowledge-store/CLAUDE.md before working with data storage in this codebase. This documentation contains essential information about how to properly use the various data structures provided by knowledge-store.
| Data Structure | Use Case |
|---|---|
| Facts | Simple key-value pairs |
| Bags | Attribute-by-attribute updates |
| Timelines | Chronologically-ordered records |
| Sequences | Numerically-ordered records |
| Sets | Collections with unique identifiers |
| Counters | Numerical counters |
Anti-Pattern Example:
// ❌ INCORRECT - NEVER DO THIS:
const dynamoDb = new AWS.DynamoDB.DocumentClient();
await dynamoDb.put({...});
// ✅ CORRECT - ALWAYS DO THIS:
await knowledgeStore.putFact('tag', key, data);| Command | Description | Example |
|---|---|---|
| Build | yarn build |
Builds all modules |
| Test all | yarn test |
Runs all tests |
| Test module | yarn test modules/foo |
Tests a specific module |
| Test file | yarn test modules/foo/tests/specific-file.spec |
Tests a specific file |
| Lint fix | yarn lint:fix |
Auto-fix lint errors |
When encountering lint errors, USE yarn lint:fix as the first approach - it will auto-fix approximately 95% of lint issues. Only manually address lint errors that cannot be auto-fixed.
⚠️ IMPORTANT: ALWAYS run builds from the repository root, NEVER from individual module directories.
- The build tool (build-raptor) automatically builds all dependencies in the correct order
- If there's a compilation error, there's no need to manually build dependent modules - build-raptor has already handled this
✅ TypeScript:
- MUST USE strict mode
- MUST USE
unknowninstead ofany - DO NOT USE type assertions (
as,!)
✅ Imports:
- MUST USE
import typefor types - MUST USE named exports over default exports
- MUST FOLLOW sorting by
simple-import-sort
✅ Formatting:
- 120 char line length
- Single quotes
- No semicolons
✅ Naming:
- camelCase for variables/functions/methods
- PascalCase for types/interfaces/classes
- kebab-case for filenames
✅ Files:
- MUST USE kebab-case for filenames
- MUST USE
.spec.ts,.e2e.spec.ts, or.contract.tsfor tests
✅ Type Safety:
- MUST USE
Record<string, never>instead of{}for empty objects - MUST USE
Partial<Record<string, T>>instead ofRecord<string, T>for string key objects - MUST USE underscores for numeric literals (e.g.,
60_000instead of60000) - MUST USE camelCase for constants, not ALL_CAPS
✅ Specific Rules:
- ❌ NEVER USE direct console logs (use logger module instead)
- ❌ NEVER TEST internal implementation details (use public interfaces)
- ❌ NEVER USE ad-hoc mocking when pre-built test doubles are available
When creating new API declarations in a module, you MUST update the module's index.ts to re-export the declarations. This makes the API available to external modules that import from the package.
Example:
// ✅ CORRECT - In modules/weather-api/src/index.ts
export * from './weather-api'
export * from './existing-exports'
export * from './get-weather-forecast-api' // New API declaration
// ❌ INCORRECT - Forgetting to update index.ts will cause import errors in other modulesFailing to update the index exports will result in import errors when other modules try to access the newly added API declarations.
For asynchronous operations, follow these rules:
-
For arrays of unknown/dynamic length (determined at runtime):
- MUST USE
promises(array)from@moojo/misc - CAN SPECIFY a concurrency limit using ONE of these patterns:
promises(items).map(fn).reify(10)- Pass concurrency to reify() (defaults to 16 if omitted)promises(items).forEach(10, fn)- First parameter to forEach() is the concurrency limit
- It's acceptable to use the default concurrency values if appropriate for your use case
- MUST USE
-
For arrays of fixed/known length (determinable at development time):
- MAY USE
Promise.all()only if the array has a small fixed size (e.g., 2-3 items) - SHOULD USE
promises(array)if there's any doubt about the array size
- MAY USE
// ✅ CORRECT - For dynamic arrays using reify() pattern with explicit concurrency
import { promises } from '@moojo/misc'
// This array comes from a database or user input - unknown length at development time
const results = await promises(dynamicItems)
.map(async item => processItem(item))
.reify(5) // Explicitly limit to 5 concurrent operations
// ✅ CORRECT - Using default concurrency (16)
const results = await promises(dynamicItems)
.map(async item => processItem(item))
.reify() // Uses default concurrency of 16
// ✅ CORRECT - For dynamic arrays using forEach() pattern
await promises(dynamicItems).forEach(5, async item => {
await processItem(item)
// Note: forEach executes immediately, no need for reify()
})
// ✅ CORRECT - For fixed length arrays
// These are just 3 specific operations we're doing in parallel
await Promise.all([saveSettings(), updateProfile(), refreshCache()])
// ❌ INCORRECT - Using Promise.all() on a dynamically sized array
const results = await Promise.all(dynamicItems.map(async item => processItem(item)))Benefits of using promises():
- Prevents overwhelming resources with too many concurrent operations
- Provides controlled concurrency through concurrency limits
- Returns results in original order even though processing may be out of order
- Offers methods like map(), filter(), forEach() with built-in concurrency management
ALWAYS USE errorLike(e) from @moojo/misc to safely extract error messages:
// ✅ CORRECT
import { errorLike } from '@moojo/misc-constructs'
try {
await someOperation()
} catch (e) {
return { status: 'fail', reason: errorLike(e).message }
}
// ❌ INCORRECT - Unsafe type assertion
try {
await someOperation()
} catch (e) {
return { status: 'fail', reason: (e as Error).message }
}This avoids the need to use any or unsafe type assertions when handling caught errors (which will always be of unknown type).
For time-related functionality, ALWAYS USE the provided time utilities:
-
For getting timestamps (Instant objects):
- MUST USE
clock.now()to get the current time (where clock is injected as a dependency) - MUST USE
instantOf()to create Instant objects from other values:instantOf('2023-09-01T12:00:00Z')- from ISO stringinstantOf(1630502400000)- from millisecondsinstantOf(new Date())- from Date object
- MUST USE
-
For time periods (Duration objects):
- MUST USE
durationOf()to create Duration objects:durationOf(5, 'seconds')durationOf(30, 'minutes')durationOf(2, 'hours')durationOf(1, 'days')
- MUST USE
// ✅ CORRECT
import { instantOf, durationOf } from '@moojo/duration'
import type { Clock } from '@moojo/misc-clock'
function createService(clock: Clock) {
// In service implementations, use the injected clock
const now = clock.now()
// Create a timestamp for tomorrow
const tomorrow = now.plus(durationOf(1, 'days'))
// Create an Instant from ISO string
const specificTime = instantOf('2023-09-01T12:00:00Z')
// Format to ISO string
const isoString = tomorrow.format()
}
// ❌ INCORRECT - Using JavaScript Date directly
const now = new Date()
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000)
const isoString = tomorrow.toISOString()Important notes:
- Always inject a Clock instance in your services rather than calling global time functions
- This enables proper testing with ControlledClock from '@moojo/controlled-clock'
- Use the proper unit names with durationOf: 'millis', 'seconds', 'minutes', 'hours', 'days'
- MUST USE kebab-case for filenames that match their primary export
- MUST USE named exports instead of default exports for React components
// ✅ CORRECT - In user-profile.tsx
export function UserProfile() {
// Component implementation
}
// ❌ INCORRECT - Default export
export default function UserProfile() {
// Component implementation
}This recipe walks through adding a "getWeatherForecast" endpoint to the Weather Service (modules/weather-service and modules/weather-api).
-
Create a new file
get-weather-forecast-api.tsinmodules/weather-api/src/:import { endpointDeclaration } from '@moojo/endpoints' import { z } from 'zod' // Define request schema with Zod export const GetWeatherForecastRequest = z.object({ location: z.string(), days: z.number().int().positive().optional(), }) export type GetWeatherForecastRequest = z.infer<typeof GetWeatherForecastRequest> // Define response schema with Zod export const GetWeatherForecastResponse = z.object({ forecasts: z.array( z.object({ date: z.string(), temperature: z.number(), conditions: z.string(), precipitation: z.number(), }), ), }) export type GetWeatherForecastResponse = z.infer<typeof GetWeatherForecastResponse> // Create the endpoint declaration export const getWeatherForecastDeclaration = endpointDeclaration( GetWeatherForecastRequest, GetWeatherForecastResponse, )
-
Update
modules/weather-api/src/index.tsto export the new API:export * from './weather-api' export * from './existing-exports' export * from './get-weather-forecast-api'
-
Add the endpoint to the service declaration in
modules/weather-api/src/weather-api.ts:import { getWeatherForecastDeclaration } from './get-weather-forecast-api' export const WeatherServiceDeclaration = { 'existing-endpoint': existingEndpointDeclaration, 'get-weather-forecast': getWeatherForecastDeclaration, }
-
Create a new file
get-weather-forecast-endpoint.tsinmodules/weather-service/src/:import type { GetWeatherForecastRequest, GetWeatherForecastResponse } from '@moojo/weather-api' import type { Logger } from '@moojo/logger' import type { Clock } from '@moojo/misc-clock' import { durationOf } from '@moojo/duration' export class GetWeatherForecastEndpoint { constructor( private readonly logger: Logger, private readonly clock: Clock, // Inject clock for time operations ) {} async handle(request: GetWeatherForecastRequest): Promise<GetWeatherForecastResponse> { const days = request.days || 5 // Default to 5-day forecast this.logger.info('Handling get weather forecast request', { location: request.location, days, }) // Get current time using the injected clock const now = this.clock.now() // Example simple implementation (simulated data) const forecasts = Array.from({ length: days }, (_, i) => { // Add days to the current time const forecastDate = now.plus(durationOf(i, 'days')) return { date: forecastDate.format().split('T')[0], // Get YYYY-MM-DD temperature: 15 + Math.floor(Math.random() * 10), conditions: ['Sunny', 'Cloudy', 'Rainy', 'Partly Cloudy'][Math.floor(Math.random() * 4)], precipitation: Math.random() * 100, } }) return { forecasts } } }
-
Import and register the endpoint in
modules/weather-service/src/weather-service.ts:import { WeatherServiceDeclaration, getWeatherForecastDeclaration } from '@moojo/weather-api' import { bindDeclaration } from '@moojo/endpoints' import { GetWeatherForecastEndpoint } from './get-weather-forecast-endpoint' export function createService( logger: Logger, clock: Clock, // Clock dependency // Other dependencies ) { // Create endpoint instances const getWeatherForecast = new GetWeatherForecastEndpoint(logger, clock) // Create bound service const boundService = { 'existing-endpoint': bindDeclaration(existingEndpointDeclaration, existingEndpoint), 'get-weather-forecast': bindDeclaration(getWeatherForecastDeclaration, getWeatherForecast), } return new Service(logger, quotaName, boundService, ingester, clock) }
-
Create a test file at
modules/weather-service/tests/get-weather-forecast-endpoint.spec.ts:import { GetWeatherForecastEndpoint } from '../src/get-weather-forecast-endpoint' import { InmemoryLogger } from '@moojo/logger' import { ControlledClock } from '@moojo/controlled-clock' import { instantOf } from '@moojo/duration' describe('GetWeatherForecastEndpoint', () => { async function init() { const logger = new InmemoryLogger() const clock = new ControlledClock(instantOf('2023-01-01T00:00:00Z')) const endpoint = new GetWeatherForecastEndpoint(logger, clock) return { endpoint, logger, clock } } it('returns default 5-day forecast when days not specified', async () => { const { endpoint } = await init() const result = await endpoint.handle({ location: 'New York', }) expect(result.forecasts).toHaveLength(5) expect(result.forecasts[0].temperature).toBeDefined() expect(result.forecasts[0].conditions).toBeDefined() }) it('respects requested number of days', async () => { const { endpoint } = await init() const result = await endpoint.handle({ location: 'New York', days: 3, }) expect(result.forecasts).toHaveLength(3) }) })
-
Build the modules:
yarn build -g modules/weather-service
-
Run the tests:
yarn test -g modules/weather-service
⚠️ CRITICAL: NEVER test internal implementation details - ONLY test through public interfaces (endpoints).
✅ Structure service tests to reflect how the service would be used in production:
- MUST TREAT the service as a black box - inputs go in, assertions are made on outputs
- NEVER directly verify data in storage; test the retrieval endpoints instead
- For multi-step flows, use endpoint chaining to verify data persistence
✅ Test Setup:
- MUST USE an
init()function to create the service under test with its dependencies - This function MUST RETURN an object with all fully initialized components
- Each test MUST BEGIN with a call to
init() - The returned object SHOULD INCLUDE a ServiceClient initialized from the service's handler
✅ Service Interaction and Test Doubles:
- MUST USE ServiceClient to interact with endpoints (not calling handler methods directly)
- MUST USE established test doubles: InmemoryItemStore, MockLlmClient, ControlledClock, InmemoryLogger, etc.
- NEVER USE ad-hoc mocking approaches when pre-built test doubles are available
For end-to-end tests that cover subsystems (collections of services), STRONGLY CONSIDER placing new test cases in the dedicated e2e testing modules:
| Module | Subsystem Coverage |
|---|---|
user-journey-tests |
User-facing flows |
ci-cd-tests |
CI/CD pipeline |
These modules are the preferred location for new e2e test cases that span multiple services within their respective subsystems.