Skip to content

Instantly share code, notes, and snippets.

@imaman
Last active January 15, 2026 06:54
Show Gist options
  • Select an option

  • Save imaman/0c175fdaf37bff7e51b3482a0a0be2d7 to your computer and use it in GitHub Desktop.

Select an option

Save imaman/0c175fdaf37bff7e51b3482a0a0be2d7 to your computer and use it in GitHub Desktop.
our CLAUDE.md file

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Table of Contents


Quick Reference

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 Rules

⚠️ CRITICAL: knowledge-store is the ONLY storage solution to use in this project.

⚠️ CRITICAL: Use promises() 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's index.ts file.


Storage Solution

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);

Build/Test Commands

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

Fixing 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.

Build System Notes

⚠️ 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

Code Style Guidelines

TypeScript:

  • MUST USE strict mode
  • MUST USE unknown instead of any
  • DO NOT USE type assertions (as, !)

Imports:

  • MUST USE import type for 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.ts for tests

Type Safety:

  • MUST USE Record<string, never> instead of {} for empty objects
  • MUST USE Partial<Record<string, T>> instead of Record<string, T> for string key objects
  • MUST USE underscores for numeric literals (e.g., 60_000 instead of 60000)
  • 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

Best Practices

API Module Exports

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 modules

Failing to update the index exports will result in import errors when other modules try to access the newly added API declarations.

Concurrency Management

For asynchronous operations, follow these rules:

  1. 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
  2. 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
// ✅ 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

Error Handling

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).

Time Representation

For time-related functionality, ALWAYS USE the provided time utilities:

  1. 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 string
      • instantOf(1630502400000) - from milliseconds
      • instantOf(new Date()) - from Date object
  2. For time periods (Duration objects):

    • MUST USE durationOf() to create Duration objects:
      • durationOf(5, 'seconds')
      • durationOf(30, 'minutes')
      • durationOf(2, 'hours')
      • durationOf(1, 'days')
// ✅ 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'

React Component Conventions

  • 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
}

Adding a New Endpoint to a Service

This recipe walks through adding a "getWeatherForecast" endpoint to the Weather Service (modules/weather-service and modules/weather-api).

Step 1: Define the API (in the weather-api module)

  1. Create a new file get-weather-forecast-api.ts in modules/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,
    )
  2. Update modules/weather-api/src/index.ts to export the new API:

    export * from './weather-api'
    export * from './existing-exports'
    export * from './get-weather-forecast-api'
  3. 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,
    }

Step 2: Implement the Endpoint (in the weather-service module)

  1. Create a new file get-weather-forecast-endpoint.ts in modules/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 }
      }
    }
  2. 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)
    }

Step 3: Write Tests

  1. 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)
      })
    })

Step 4: Build and Test

  1. Build the modules:

    yarn build -g modules/weather-service
  2. Run the tests:

    yarn test -g modules/weather-service

Service Testing Conventions

⚠️ 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

E2E Testing Modules

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment