Skip to content

ADR-007: Auto-Generated Multi-Language SDK for LBS Foundry API

Status

Accepted & Implemented - January 2025

  • Phase 1-3: TypeScript SDK generator and client (Completed)
  • Phase 4: Python SDK support (Completed)
  • Phase 5: Import API support (Completed)

Context

Current State

LBS Foundry exposes a CQRS-based API with strongly-typed queries and commands in C#:

  • Query Endpoint: POST /api/readmodel - Accepts dynamic query objects with type discrimination via queryType property
  • Command Endpoint: POST /api/command - Executes domain commands with strong typing
  • Type System: C# classes decorated with [DataContract(Name = "...")] attributes for serialization
  • Authentication: Dual system (Clerk JWT + Basic Auth) with role-based authorization

Example C# Query:

[DataContract(Name = "PlayerSearch")]
public sealed class PlayerSearchQuery : IPublicQuery, IRequireUserContext
{
    public string QueryType => this.GetType().GetQueryTypeName();
    public List<string>? Teams { get; set; }
    public string? SearchTerm { get; set; }
    public int? MinPrice { get; set; }
    public int? MaxPrice { get; set; }
    public List<string>? Positions { get; set; }
    public ClaimsPrincipal? User => UserContext.Current;
}

Current Frontend Integration:

// Manual type definitions, prone to drift
interface PlayerSearchQuery {
  queryType: string;
  teams?: string[];
  searchTerm?: string;
  minPrice?: number;
  maxPrice?: number;
  positions?: string[];
}

// Manual API calls with no type safety
const response = await axios.post('/api/readmodel', {
  query: {
    queryType: 'PlayerSearch',
    teams: ['Panthers', 'Eels'],
    minPrice: 200000
  }
});

Problems with Current Approach

  1. Type Drift: Frontend types manually maintained, easily become outdated
  2. No Compile-Time Safety: Typos in queryType or property names only caught at runtime
  3. Discoverability: Developers must reference C# code to find available queries/commands
  4. Documentation Burden: Query structure changes require manual docs updates
  5. Error-Prone: Easy to send malformed requests that pass TypeScript but fail server validation
  6. No IntelliSense: IDE cannot provide autocomplete for query properties
  7. Testing Complexity: No shared type definitions between frontend tests and API contracts

Requirements

  1. High-Fidelity Types: TypeScript types must exactly match C# contracts (properties, nullability, types)
  2. Automated Generation: Types generated from C# source code during build process
  3. Lightweight Client: Minimal runtime overhead, simple API surface
  4. Build Integration: Works in both GitHub Actions (CI/CD) and local development
  5. npm Distribution: Published as versioned npm package for easy consumption
  6. Developer Experience: IntelliSense support, compile-time safety, clear error messages
  7. Extensibility: Support for future features (WebSocket updates, caching strategies, etc.)

Decision

We will implement an auto-generated TypeScript SDK using a custom Roslyn-based source generator that:

  1. Uses [DataContract(Name, Namespace)] attributes as the source of truth for client-side API structure
  2. Aligns with ModuleDefinition for simple, intuitive organization (Core, Sport, SuperCoach, Ballr, Discord)
  3. Generates TypeScript types with exact property mappings including nullability
  4. Uses modern object literals (not TypeScript namespace keyword) for better tree-shaking and JavaScript compatibility
  5. Creates type-safe client methods for executing queries and commands
  6. Publishes to npm as @lbs-foundry/typescript-sdk with semantic versioning
  7. Integrates with GitHub Actions for automated generation and publishing

Key Design Decisions

Namespace Source: [DataContract(Namespace = LbsNamespace.SuperCoachQueries)] - Explicit backend control

Module Alignment: Namespaces map to ModuleDefinition (Core, Sport, SuperCoach, Ballr, Discord) - Keep it simple

Modern TypeScript: Object literals (export const queries = { core: {...}, sport: {...} }) - Not namespace keyword

Start Simple: 5-7 top-level modules, can add sub-grouping later if needed

Architecture Overview

flowchart TB
    subgraph "Build Pipeline"
        A[C# Codebase] --> B[Source Scanner]
        B --> C[Type Analyzer]
        C --> D[TypeScript Generator]
        D --> E[npm Package Builder]
        E --> F[Package Publisher]
    end

    subgraph "Generated Package"
        G[TypeScript Types]
        H[Query Builder]
        I[Command Executor]
        J[Client Configuration]
    end

    subgraph "Consumer Applications"
        K[Svelte Frontend]
        L[Node.js Services]
        M[Testing Tools]
    end

    F --> G
    F --> H
    F --> I
    F --> J

    G --> K
    H --> K
    I --> K

    G --> L
    H --> L
    I --> L

    G --> M
    H --> M
    I --> M

Implementation Plan

Phase 1: Type Scanner & Generator (Week 1)

Goal: Create tool to extract C# types and generate TypeScript

Code Generation Pipeline

The SDK generation follows this pipeline:

┌─────────────────────────────────────────────────────────────────────┐
│ 1. BUILD C# ASSEMBLIES                                              │
│    dotnet build LBS.slnx                                            │
│    → Produces: LBS.Domain.Core.dll, LBS.Ballr.Core.dll, etc.       │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 2. RUN TYPE SCANNER (LBS.Tools.TypeScriptGenerator)                │
│    dotnet run --project src/Tools/LBS.Tools.TypeScriptGenerator    │
│    --assemblies "src/Domain/*/bin/Debug/net10.0/*.dll"              │
│    --output "./sdk/typescript/src/generated"                        │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 3. SCAN ASSEMBLIES WITH ROSLYN                                      │
│    • Load each DLL with MetadataLoadContext                         │
│    • Find all types with [DataContract] attribute                   │
│    • Filter for IQuery, ICommand, response contracts               │
│    • Extract properties, XML docs, RequiresRoles, etc.             │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 4. ANALYZE & GROUP TYPES                                            │
│    • Extract DataContract Name and Namespace                        │
│    • Parse namespace: "LBS.Foundry.Queries.SuperCoach"             │
│      → Module: "superCoach"                                         │
│    • Group queries by module (core, sport, superCoach, etc.)       │
│    • Group commands by module                                       │
│    • Build type dependency graph                                    │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 5. GENERATE TYPESCRIPT FILES                                        │
│    Generated files:                                                 │
│    • src/generated/types/queries.ts     (all query interfaces)     │
│    • src/generated/types/commands.ts    (all command interfaces)   │
│    • src/generated/types/responses.ts   (all response interfaces)  │
│    • src/generated/queries.ts           (query builder object)     │
│    • src/generated/commands.ts          (command builder object)   │
│    • src/generated/constants.ts         (RoleDefinition, etc.)     │
│    • src/generated/flat.ts              (flat exports)             │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 6. BUILD NPM PACKAGE                                                │
│    cd sdk/typescript                                                │
│    npm install                                                      │
│    npm run build     (TypeScript → JavaScript + .d.ts files)       │
│    npm run test      (Run tests against generated code)            │
│    → Produces: dist/ folder with compiled JS + type definitions    │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 7. PUBLISH TO GITHUB PACKAGES (PRIVATE)                            │
│    npm publish                                                     │
│    → Published: @your-github-org/lbs-foundry-typescript-sdk@...   │
│    → Accessible only to GitHub org members with valid token       │
└─────────────────────────────────────────────────────────────────────┘

Detailed Generator Implementation

Tasks:

  1. Create console app LBS.Tools.TypeScriptGenerator:
// Program.cs
public class Program
{
    public static async Task Main(string[] args)
    {
        var config = ParseArgs(args);

        // Step 1: Scan assemblies
        var assemblies = LoadAssemblies(config.AssemblyPaths);

        // Step 2: Extract types
        var scanner = new TypeScanner();
        var queries = scanner.FindQueries(assemblies);
        var commands = scanner.FindCommands(assemblies);
        var responseContracts = scanner.FindResponseContracts(assemblies);

        // Step 3: Generate TypeScript
        var generator = new TypeScriptGenerator(config);
        await generator.GenerateTypesAsync(queries, commands, responseContracts);
        await generator.GenerateBuildersAsync(queries, commands);
        await generator.GenerateConstantsAsync(assemblies);

        Console.WriteLine($"Generated TypeScript SDK at {config.OutputPath}");
    }
}
  1. Use Roslyn/MetadataLoadContext to scan assemblies:
public class TypeScanner
{
    public List<TypeInfo> FindQueries(IEnumerable<Assembly> assemblies)
    {
        var queries = new List<TypeInfo>();

        foreach (var assembly in assemblies)
        {
            // Find all types with [DataContract] that implement IQuery interfaces
            var types = assembly.GetTypes()
                .Where(t => t.GetCustomAttribute<DataContractAttribute>() != null)
                .Where(t => t.GetInterfaces().Any(i =>
                    i.Name == "IQuery" ||
                    i.Name == "IPublicQuery" ||
                    i.Name == "ISecureQuery"))
                .ToList();

            foreach (var type in types)
            {
                var typeInfo = AnalyzeType(type);
                queries.Add(typeInfo);
            }
        }

        return queries;
    }

    private TypeInfo AnalyzeType(Type type)
    {
        // Extract DataContract attribute
        var dataContract = type.GetCustomAttribute<DataContractAttribute>();

        // Extract RequiresRoles attribute
        var requiresRoles = type.GetCustomAttribute<RequiresRolesAttribute>();

        // Extract properties
        var properties = type.GetProperties()
            .Where(p => p.GetCustomAttribute<DataMemberAttribute>() != null)
            .Select(p => new PropertyInfo
            {
                Name = p.Name,
                Type = p.PropertyType,
                IsOptional = IsNullable(p.PropertyType),
                XmlDoc = GetXmlDoc(p)
            })
            .ToList();

        return new TypeInfo
        {
            Name = dataContract?.Name ?? type.Name,
            Namespace = dataContract?.Namespace ?? "LBS.Foundry",
            Properties = properties,
            RequiredRoles = requiresRoles?.Roles ?? Array.Empty<string>(),
            XmlDoc = GetXmlDoc(type),
            IsQuery = IsQueryType(type),
            IsCommand = IsCommandType(type)
        };
    }
}
  1. Build type mapping engine:
public class TypeMapper
{
    public string MapToTypeScript(Type csharpType, bool isOptional)
    {
        var typeScriptType = csharpType switch
        {
            _ when csharpType == typeof(string) => "string",
            _ when csharpType == typeof(int) => "number",
            _ when csharpType == typeof(long) => "number",
            _ when csharpType == typeof(decimal) => "number",
            _ when csharpType == typeof(double) => "number",
            _ when csharpType == typeof(float) => "number",
            _ when csharpType == typeof(bool) => "boolean",
            _ when csharpType == typeof(Guid) => "string",
            _ when csharpType == typeof(DateTime) => "string",
            _ when csharpType == typeof(DateTimeOffset) => "string",

            // Nullable types
            _ when IsNullable(csharpType) =>
                MapToTypeScript(Nullable.GetUnderlyingType(csharpType)!, true),

            // Arrays and Lists
            _ when IsEnumerable(csharpType) =>
                $"{MapToTypeScript(GetElementType(csharpType), false)}[]",

            // Complex types (other contracts)
            _ => csharpType.Name
        };

        return isOptional ? $"{typeScriptType} | undefined" : typeScriptType;
    }

    private bool IsNullable(Type type) =>
        Nullable.GetUnderlyingType(type) != null ||
        !type.IsValueType;
}
  1. Generate TypeScript interfaces:
public class TypeScriptGenerator
{
    public async Task GenerateTypesAsync(
        List<TypeInfo> queries,
        List<TypeInfo> commands,
        List<TypeInfo> responses)
    {
        // Generate query interfaces
        var queryContent = new StringBuilder();
        queryContent.AppendLine("// Auto-generated query types");
        queryContent.AppendLine("// DO NOT EDIT - Changes will be overwritten\n");

        foreach (var query in queries)
        {
            queryContent.AppendLine(GenerateInterface(query));
        }

        await File.WriteAllTextAsync(
            Path.Combine(config.OutputPath, "types", "queries.ts"),
            queryContent.ToString()
        );

        // Similar for commands and responses...
    }

    private string GenerateInterface(TypeInfo typeInfo)
    {
        var sb = new StringBuilder();

        // Add JSDoc from XML comments
        if (!string.IsNullOrEmpty(typeInfo.XmlDoc))
        {
            sb.AppendLine("/**");
            sb.AppendLine($" * {typeInfo.XmlDoc}");
            if (typeInfo.RequiredRoles.Any())
            {
                sb.AppendLine($" * @requires {string.Join(", ", typeInfo.RequiredRoles)}");
            }
            sb.AppendLine(" */");
        }

        sb.AppendLine($"export interface {typeInfo.Name} {{");

        foreach (var prop in typeInfo.Properties)
        {
            // Add property JSDoc
            if (!string.IsNullOrEmpty(prop.XmlDoc))
            {
                sb.AppendLine($"  /** {prop.XmlDoc} */");
            }

            // Generate property
            var optional = prop.IsOptional ? "?" : "";
            var tsType = typeMapper.MapToTypeScript(prop.Type, prop.IsOptional);
            sb.AppendLine($"  {ToCamelCase(prop.Name)}{optional}: {tsType};");
        }

        sb.AppendLine("}");

        return sb.ToString();
    }
}
  1. Generate builder objects (queries & commands):
public async Task GenerateBuildersAsync(List<TypeInfo> queries, List<TypeInfo> commands)
{
    // Group by module
    var queryModules = queries
        .GroupBy(q => ExtractModule(q.Namespace))
        .OrderBy(g => g.Key);

    var sb = new StringBuilder();
    sb.AppendLine("// Auto-generated query builders");
    sb.AppendLine("import * as Types from './types/queries';");
    sb.AppendLine();
    sb.AppendLine("export const queries = {");

    foreach (var module in queryModules)
    {
        sb.AppendLine($"  {module.Key}: {{");

        foreach (var query in module)
        {
            var builderName = ToCamelCase(query.Name.Replace("Query", ""));
            var hasParams = query.Properties.Any(p => p.Name != "QueryType");

            if (hasParams)
            {
                sb.AppendLine($"    {builderName}: (params: Omit<Types.{query.Name}, 'queryType'>): Types.{query.Name} => ({{");
                sb.AppendLine($"      queryType: '{query.Name.Replace("Query", "")}',");
                sb.AppendLine($"      ...params");
                sb.AppendLine($"    }}),");
            }
            else
            {
                sb.AppendLine($"    {builderName}: (): Types.{query.Name} => ({{");
                sb.AppendLine($"      queryType: '{query.Name.Replace("Query", "")}'");
                sb.AppendLine($"    }}),");
            }
        }

        sb.AppendLine("  },");
    }

    sb.AppendLine("} as const;");

    await File.WriteAllTextAsync(
        Path.Combine(config.OutputPath, "queries.ts"),
        sb.ToString()
    );
}

private string ExtractModule(string dataContractNamespace)
{
    // "LBS.Foundry.Queries.SuperCoach" → "superCoach"
    var parts = dataContractNamespace.Split('.');
    if (parts.Length >= 4)
    {
        return ToCamelCase(parts[3]); // "SuperCoach" → "superCoach"
    }
    return "misc";
}
  1. Preserve XML documentation as JSDoc:
public string GetXmlDoc(MemberInfo member)
{
    // Load XML documentation file
    var xmlPath = Path.ChangeExtension(member.Module.Assembly.Location, ".xml");
    if (!File.Exists(xmlPath)) return string.Empty;

    var xml = XDocument.Load(xmlPath);
    var memberName = GetXmlMemberName(member);

    var summary = xml.Descendants("member")
        .FirstOrDefault(m => m.Attribute("name")?.Value == memberName)
        ?.Element("summary")
        ?.Value
        .Trim();

    return summary ?? string.Empty;
}

Example Generated Output

Input (C#):

[DataContract(Name = "PlayerSearch", Namespace = LbsNamespace.SuperCoachQueries)]
public sealed class PlayerSearchQuery : IPublicQuery
{
    [DataMember]
    public List<string>? Teams { get; set; }

    [DataMember]
    public int? MinPrice { get; set; }
}

Output (TypeScript):

// types/queries.ts
export interface PlayerSearchQuery {
  queryType: 'PlayerSearch';
  teams?: string[];
  minPrice?: number;
}

// queries.ts
export const queries = {
  superCoach: {
    playerSearch: (params: Omit<PlayerSearchQuery, 'queryType'>): PlayerSearchQuery => ({
      queryType: 'PlayerSearch',
      ...params
    })
  }
} as const;

Example Output - Queries:

/**
 * Query to search for players with optional filters
 * Implements: IPublicQuery, IRequireUserContext
 */
export interface PlayerSearchQuery {
  /** The query type discriminator */
  queryType: 'PlayerSearch';

  /** Team names to filter by */
  teams?: string[];

  /** Search term for player name */
  searchTerm?: string;

  /** Minimum player price */
  minPrice?: number;

  /** Maximum player price */
  maxPrice?: number;

  /** Position filters */
  positions?: string[];
}

/**
 * Result contract for player search
 */
export interface PlayerSearchResult {
  id: string;
  displayName: string;
  teamName: string;
  position: string;
  price: number;
}

Example Output - Commands:

/**
 * Command to create a new user
 * Implements: DomainCommand<UserId>
 * Requires Roles: UserManager, Admin
 */
export interface CreateUserCommand {
  /** Command type discriminator */
  commandType: 'CreateUser';

  /** Aggregate root ID */
  aggregateRootId?: string;

  /** User email address */
  email: string;

  /** User first name */
  firstName?: string;

  /** User last name */
  lastName?: string;

  /** Clerk user ID for authentication */
  clerkUserId?: string;

  /** Username */
  userName?: string;

  /** Email verification status */
  emailVerified: boolean;

  /** Account creation timestamp */
  createdAt: string;

  /** User status (Active, Inactive, Suspended, Deleted) */
  status: UserStatus;

  /** Assigned roles */
  roles: string[];

  /** Account type (Human, ServiceAccount) */
  userType: AccountType;

  /** Password for service accounts */
  password?: string;
}

/**
 * Command to update participant slug name
 * Implements: DomainCommand<ParticipantId>
 * Requires Roles: Admin
 */
export interface UpdateParticipantSlugNameCommand {
  commandType: 'UpdateParticipantSlugName';
  aggregateRootId: string;
  slugName: string;
}

/**
 * Result of command execution
 */
export interface CommandResponse<TResult = void> {
  success: boolean;
  aggregateRootId?: string;
  result?: TResult;
  events?: DomainEvent[];
  errors?: CommandError[];
}

Phase 2: Client Library (Week 2)

Goal: Build lightweight, type-safe client wrapper that completely abstracts HTTP details

Architecture: Developers never manually construct HTTP requests - they only call client methods.

Tasks:

  1. Create client configuration:

    export interface FoundryClientConfig {
      baseUrl: string;
      auth?: {
        type: 'bearer' | 'basic';
        token?: string;
        username?: string;
        password?: string;
      };
      timeout?: number;
      retries?: number;
      onError?: (error: Error) => void;
    }
    

  2. Implement query executor (handles all /api/readmodel communication):

    export class FoundryQueryClient {
      private readonly config: FoundryClientConfig;
      private readonly httpClient: HttpClient;
    
      constructor(config: FoundryClientConfig) {
        this.config = config;
        this.httpClient = new HttpClient(config);
      }
    
      /**
       * Execute a query against /api/readmodel
       * Handles auth headers, error handling, response parsing automatically
       */
      async query<TQuery, TResult>(
        query: TQuery,
        options?: QueryOptions
      ): Promise<ReadModelResponse<TResult>> {
        try {
          // POST to /api/readmodel
          const response = await this.httpClient.post('/api/readmodel', {
            query,
            skip: options?.skip ?? 0,
            take: options?.take ?? 50,
            orderBy: options?.orderBy
          });
    
          return {
            data: response.data.data as TResult[],
            totalCount: response.data.totalCount,
            skip: response.data.skip,
            take: response.data.take
          };
        } catch (error) {
          this.handleError(error);
          throw error;
        }
      }
    
      /**
       * Execute a query with pagination support
       */
      async queryPaginated<TQuery, TResult>(
        query: TQuery,
        pagination: PaginationOptions
      ): Promise<PaginatedResponse<TResult>> {
        return this.query<TQuery, TResult>(query, pagination);
      }
    
      private handleError(error: any): void {
        // Centralized error handling
        if (this.config.onError) {
          this.config.onError(error);
        }
      }
    }
    

  3. Implement command executor (handles all /api/command communication):

    export class FoundryCommandClient {
      private readonly config: FoundryClientConfig;
      private readonly httpClient: HttpClient;
    
      constructor(config: FoundryClientConfig) {
        this.config = config;
        this.httpClient = new HttpClient(config);
      }
    
      /**
       * Execute a command against /api/command
       * Handles auth headers, error handling, response parsing automatically
       */
      async execute<TCommand, TResult = void>(
        command: TCommand
      ): Promise<CommandResponse<TResult>> {
        try {
          // POST to /api/command
          const response = await this.httpClient.post('/api/command', command);
    
          return {
            success: response.data.success ?? true,
            aggregateRootId: response.data.aggregateRootId,
            result: response.data.result as TResult,
            events: response.data.events,
            errors: response.data.errors
          };
        } catch (error) {
          this.handleError(error);
          throw error;
        }
      }
    
      private handleError(error: any): void {
        if (this.config.onError) {
          this.config.onError(error);
        }
      }
    }
    

  4. Create unified client (single entry point for all API calls):

    /**
     * Main client for LBS Foundry API
     * Abstracts all HTTP communication - developers never construct requests manually
     */
    export class FoundryClient {
      // Sub-clients for queries and commands
      private readonly queryClient: FoundryQueryClient;
      private readonly commandClient: FoundryCommandClient;
    
      constructor(config: FoundryClientConfig) {
        this.queryClient = new FoundryQueryClient(config);
        this.commandClient = new FoundryCommandClient(config);
      }
    
      /**
       * Execute a query
       * @example
       * const result = await client.query(queries.superCoach.playerSearch({ minPrice: 200000 }));
       */
      async query<TQuery, TResult>(
        query: TQuery,
        options?: QueryOptions
      ): Promise<ReadModelResponse<TResult>> {
        return this.queryClient.query<TQuery, TResult>(query, options);
      }
    
      /**
       * Execute a command
       * @example
       * const result = await client.execute(commands.core.createUser({ email: '...' }));
       */
      async execute<TCommand, TResult = void>(
        command: TCommand
      ): Promise<CommandResponse<TResult>> {
        return this.commandClient.execute<TCommand, TResult>(command);
      }
    
      /**
       * Execute a query with pagination
       */
      async queryPaginated<TQuery, TResult>(
        query: TQuery,
        pagination: PaginationOptions
      ): Promise<PaginatedResponse<TResult>> {
        return this.queryClient.queryPaginated<TQuery, TResult>(query, pagination);
      }
    }
    

  5. Internal HTTP client (handles auth, retries, etc.):

    class HttpClient {
      private readonly config: FoundryClientConfig;
      private readonly axiosInstance: AxiosInstance;
    
      constructor(config: FoundryClientConfig) {
        this.config = config;
        this.axiosInstance = axios.create({
          baseURL: config.baseUrl,
          timeout: config.timeout ?? 30000,
          headers: this.buildHeaders()
        });
    
        this.setupInterceptors();
      }
    
      private buildHeaders(): Record<string, string> {
        const headers: Record<string, string> = {
          'Content-Type': 'application/json'
        };
    
        if (this.config.auth?.type === 'bearer' && this.config.auth.token) {
          headers['Authorization'] = `Bearer ${this.config.auth.token}`;
        } else if (this.config.auth?.type === 'basic') {
          const credentials = btoa(`${this.config.auth.username}:${this.config.auth.password}`);
          headers['Authorization'] = `Basic ${credentials}`;
        }
    
        return headers;
      }
    
      private setupInterceptors(): void {
        // Request interceptor
        this.axiosInstance.interceptors.request.use(
          (config) => {
            // Add any request modifications here
            return config;
          },
          (error) => Promise.reject(error)
        );
    
        // Response interceptor
        this.axiosInstance.interceptors.response.use(
          (response) => response,
          async (error) => {
            // Retry logic
            if (this.config.retries && this.shouldRetry(error)) {
              return this.retry(error.config);
            }
            return Promise.reject(error);
          }
        );
      }
    
      async post<T = any>(url: string, data: any): Promise<AxiosResponse<T>> {
        return this.axiosInstance.post<T>(url, data);
      }
    
      private shouldRetry(error: any): boolean {
        // Retry on network errors or 5xx server errors
        return !error.response || (error.response.status >= 500 && error.response.status < 600);
      }
    
      private async retry(config: any, attemptNumber: number = 1): Promise<any> {
        if (attemptNumber > (this.config.retries ?? 3)) {
          throw new Error('Max retries exceeded');
        }
    
        // Exponential backoff
        const delay = Math.min(1000 * Math.pow(2, attemptNumber), 10000);
        await new Promise(resolve => setTimeout(resolve, delay));
    
        return this.axiosInstance.request(config);
      }
    }
    

Complete Usage Example (developers never see HTTP details):

import { FoundryClient, queries, commands } from '@lbs-foundry/typescript-sdk';

// 1. Initialize client ONCE (typically in a singleton or context)
const client = new FoundryClient({
  baseUrl: 'https://api.ballr.live',
  auth: {
    type: 'bearer',
    token: 'your-jwt-token'
  },
  timeout: 30000,
  retries: 3,
  onError: (error) => {
    console.error('API Error:', error);
    // Send to error tracking service
  }
});

// 2. Execute queries - NO manual HTTP calls!
const players = await client.query(
  queries.superCoach.playerSearch({
    teams: ['Panthers'],
    minPrice: 200000
  })
);

console.log('Found players:', players.data);
console.log('Total:', players.totalCount);

// 3. Execute commands - NO manual HTTP calls!
const result = await client.execute(
  commands.core.createUser({
    email: 'user@example.com',
    firstName: 'John',
    lastName: 'Doe',
    emailVerified: true,
    createdAt: new Date().toISOString(),
    status: UserStatus.Active,
    roles: [RoleDefinition.Member],
    userType: AccountType.Human
  })
);

console.log('Created user:', result.aggregateRootId);

// 4. Pagination support - NO manual HTTP calls!
const paginatedPlayers = await client.queryPaginated(
  queries.superCoach.playerSearch({ minPrice: 200000 }),
  { skip: 0, take: 20, orderBy: 'displayName' }
);

React Integration Example:

// Create client instance (typically in a context or hook)
const foundryClient = new FoundryClient({
  baseUrl: process.env.REACT_APP_API_URL!,
  auth: { type: 'bearer', token: authToken }
});

// Use with React Query - developers only call client methods
function PlayerList() {
  const { data, isLoading } = useQuery({
    queryKey: ['players', 'search'],
    queryFn: () => foundryClient.query(
      queries.superCoach.playerSearch({ minPrice: 200000 })
    )
  });

  // Use with mutations
  const createUserMutation = useMutation({
    mutationFn: (userData: CreateUserData) =>
      foundryClient.execute(
        commands.core.createUser(userData)
      )
  });

  // All HTTP details handled by the client!
  return <div>...</div>;
}

Key Benefits of Client Executors:

  1. Zero HTTP Boilerplate - Developers never write fetch or axios calls
  2. Centralized Auth - Configure once, works everywhere
  3. Automatic Retries - Built-in retry logic with exponential backoff
  4. Type Safety - Request and response types fully typed
  5. Error Handling - Centralized error handling and logging
  6. Testing - Easy to mock FoundryClient for tests
  7. Consistency - All API calls follow the same pattern

Phase 3: Developer Experience Enhancements (Week 2)

Goal: Maximize type safety and IntelliSense

Tasks:

  1. Design Namespacing Strategy:

The SDK will use the [DataContract] attribute's Name and Namespace properties as the source of truth for TypeScript namespace generation. This gives backend developers explicit control over the client-side API structure.

C# Namespace Declaration - Aligned with ModuleDefinition:

Start simple by aligning with your existing ModuleDefinition (Core, SportCore, SuperCoachNrl, BallrContests, etc.):

// LBS.Domain.Core/LbsNamespace.cs
public static class LbsNamespace
{
    public const string Default = "LBS.Foundry";

    // Query Namespaces - Aligned with ModuleDefinition
    public const string CoreQueries = "LBS.Foundry.Queries.Core";
    public const string SportQueries = "LBS.Foundry.Queries.Sport";
    public const string SuperCoachQueries = "LBS.Foundry.Queries.SuperCoach";
    public const string BallrQueries = "LBS.Foundry.Queries.Ballr";
    public const string DiscordQueries = "LBS.Foundry.Queries.Discord";

    // Command Namespaces - Aligned with ModuleDefinition
    public const string CoreCommands = "LBS.Foundry.Commands.Core";
    public const string SportCommands = "LBS.Foundry.Commands.Sport";
    public const string SuperCoachCommands = "LBS.Foundry.Commands.SuperCoach";
    public const string BallrCommands = "LBS.Foundry.Commands.Ballr";
    public const string DiscordCommands = "LBS.Foundry.Commands.Discord";
}

Module Mapping: - ModuleDefinition.CoreLbsNamespace.CoreQueries/Commands - ModuleDefinition.SportCoreLbsNamespace.SportQueries/Commands - ModuleDefinition.SuperCoachNrlLbsNamespace.SuperCoachQueries/Commands - ModuleDefinition.BallrContestsLbsNamespace.BallrQueries/Commands - ModuleDefinition.DiscordLbsNamespace.DiscordQueries/Commands

Apply to Queries and Commands:

// SuperCoach queries
[DataContract(Name = "PlayerSearch", Namespace = LbsNamespace.SuperCoachQueries)]
public sealed class PlayerSearchQuery : IPublicQuery, IRequireUserContext
{
    public string QueryType => this.GetType().GetQueryTypeName();
    // ...
}

[DataContract(Name = "PlayerComparison", Namespace = LbsNamespace.SuperCoachQueries)]
public sealed class PlayerComparisonQuery : IPublicQuery, IRequireUserContext { }

// Core queries
[DataContract(Name = "CurrentUser", Namespace = LbsNamespace.CoreQueries)]
public class CurrentUserQuery : ISecureQuery, IRequireUserContext { }

// Sport queries
[DataContract(Name = "GetCompetitionBySport", Namespace = LbsNamespace.SportQueries)]
public class GetCompetitionBySportQuery : IPublicQuery { }

// Core commands
[DataContract(Name = "CreateUser", Namespace = LbsNamespace.CoreCommands)]
[RequiresRoles(RoleDefinition.UserManager, RoleDefinition.Admin)]
public sealed record CreateUserCommand : DomainCommand<UserId> { }

// Sport commands
[DataContract(Name = "CreateTeam", Namespace = LbsNamespace.SportCommands)]
public sealed record CreateTeamCommand : DomainCommand<TeamId> { }

// Ballr commands
[DataContract(Name = "CreateContest", Namespace = LbsNamespace.BallrCommands)]
public sealed record CreateContestCommand : DomainCommand<ContestId> { }

TypeScript Generation Logic:

The generator parses the namespace string to build the hierarchy:

// In the generator tool
string dataContractNamespace = GetDataContractNamespace(type); // "LBS.Foundry.Queries.Ballr.SuperCoach"
string[] parts = dataContractNamespace.Split('.');

// Extract the relevant parts after "LBS.Foundry"
// "LBS.Foundry.Queries.Ballr.SuperCoach" → ["Queries", "Ballr", "SuperCoach"]
var namespaceParts = parts.Skip(2).ToArray();

// Determine if it's a query or command
bool isQuery = namespaceParts[0] == "Queries";
bool isCommand = namespaceParts[0] == "Commands";

// Extract the path: ["Ballr", "SuperCoach"]
var path = namespaceParts.Skip(1).Select(CamelCase).ToArray(); // ["ballr", "superCoach"]

// Generate TypeScript namespace
// queries.ballr.superCoach.playerSearch

Generated TypeScript Structure (Modern ES Module Pattern):

Modern TypeScript/JavaScript Preference: Use object literals instead of the legacy namespace keyword.

// MODERN APPROACH - Object literals with ES modules
// File: src/generated/queries.ts

// Auto-generated from DataContract Namespace attributes
export const queries = {
  // Core module queries (ModuleDefinition.Core → LbsNamespace.CoreQueries)
  core: {
    currentUser: (): CurrentUserQuery => ({ queryType: 'CurrentUser' }),
    discordVerificationStatus: (params: Omit<DiscordVerificationStatusQuery, 'queryType'>): DiscordVerificationStatusQuery => ({
      queryType: 'DiscordVerificationStatus',
      ...params
    }),
  },

  // Sport module queries (ModuleDefinition.SportCore → LbsNamespace.SportQueries)
  sport: {
    getCompetitionBySport: (params: Omit<GetCompetitionBySportQuery, 'queryType'>): GetCompetitionBySportQuery => ({
      queryType: 'GetCompetitionBySport',
      ...params
    }),
    getGroups: (params: Omit<GetGroupsQuery, 'queryType'>): GetGroupsQuery => ({
      queryType: 'GetGroups',
      ...params
    }),
  },

  // SuperCoach module queries (ModuleDefinition.SuperCoachNrl → LbsNamespace.SuperCoachQueries)
  superCoach: {
    playerSearch: (params: Omit<PlayerSearchQuery, 'queryType'>): PlayerSearchQuery => ({
      queryType: 'PlayerSearch',
      ...params
    }),
    playerComparison: (params: Omit<PlayerComparisonQuery, 'queryType'>): PlayerComparisonQuery => ({
      queryType: 'PlayerComparison',
      ...params
    }),
  },

  // Ballr module queries (ModuleDefinition.BallrContests → LbsNamespace.BallrQueries)
  ballr: {
    listContests: (params: Omit<BallrContestsQuery, 'queryType'>): BallrContestsQuery => ({
      queryType: 'BallrContests',
      ...params
    }),
    getContestById: (params: Omit<GetBallrContestByIdQuery, 'queryType'>): GetBallrContestByIdQuery => ({
      queryType: 'GetBallrContestById',
      ...params
    }),
  },

  // Discord module queries (ModuleDefinition.Discord → LbsNamespace.DiscordQueries)
  discord: {
    // Discord queries here
  },
} as const;

// File: src/generated/commands.ts
export const commands = {
  // Core module commands
  core: {
    createUser: (params: Omit<CreateUserCommand, 'commandType'>): CreateUserCommand => ({
      commandType: 'CreateUser',
      ...params
    }),
    updateUser: (params: Omit<UpdateUserCommand, 'commandType'>): UpdateUserCommand => ({
      commandType: 'UpdateUser',
      ...params
    }),
    deleteUser: (params: Omit<DeleteUserCommand, 'commandType'>): DeleteUserCommand => ({
      commandType: 'DeleteUser',
      ...params
    }),
  },

  // Sport module commands
  sport: {
    createTeam: (params: Omit<CreateTeamCommand, 'commandType'>): CreateTeamCommand => ({
      commandType: 'CreateTeam',
      ...params
    }),
    createParticipant: (params: Omit<CreateParticipantCommand, 'commandType'>): CreateParticipantCommand => ({
      commandType: 'CreateParticipant',
      ...params
    }),
    createSportingEvent: (params: Omit<CreateSportingEventCommand, 'commandType'>): CreateSportingEventCommand => ({
      commandType: 'CreateSportingEvent',
      ...params
    }),
  },

  // SuperCoach module commands
  superCoach: {
    setPlayerStats: (params: Omit<SetSuperCoachParticipantFantasyStatsCommand, 'commandType'>): SetSuperCoachParticipantFantasyStatsCommand => ({
      commandType: 'SetSuperCoachParticipantFantasyStats',
      ...params
    }),
  },

  // Ballr module commands
  ballr: {
    createContest: (params: Omit<CreateContestCommand, 'commandType'>): CreateContestCommand => ({
      commandType: 'CreateContest',
      ...params
    }),
  },

  // Discord module commands
  discord: {
    // Discord commands here
  },
} as const;

Why Object Literals Instead of namespace Keyword?

Pattern Status Reason
namespace keyword Legacy TypeScript-specific, pre-ES6 solution
Object literals Modern Standard JavaScript, works everywhere, tree-shakeable
// OLD WAY (TypeScript namespaces - legacy)
export namespace queries {
  export namespace core {
    export const currentUser = () => ({ ... });
  }
}

// NEW WAY (Object literals - modern)
export const queries = {
  core: {
    currentUser: () => ({ ... })
  }
};

Benefits of Object Literal Approach: 1. Standard JavaScript - Works in any JS environment, not just TypeScript 2. Better Tree-Shaking - Bundlers can eliminate unused code more easily 3. Simpler Syntax - Less TypeScript-specific syntax to learn 4. Runtime Value - Can be inspected, iterated, and manipulated at runtime 5. Industry Standard - Modern libraries use this pattern (React Query, Zustand, etc.)

Usage Examples:

import { queries, commands } from '@lbs-foundry/typescript-sdk';

// Simple, module-aligned access
const players = await client.query(
  queries.superCoach.playerSearch({ minPrice: 200000 })
);

const user = await client.execute(
  commands.core.createUser({
    email: 'user@example.com',
    status: UserStatus.Active,
    roles: [RoleDefinition.Member],
    userType: AccountType.Human,
    emailVerified: false,
    createdAt: new Date().toISOString(),
  })
);

const team = await client.execute(
  commands.sport.createTeam({
    displayName: 'Panthers',
    // ...
  })
);

// IntelliSense shows you all available modules
queries. // → { core, sport, superCoach, ballr, discord }
queries.sport. // → { getCompetitionBySport, getGroups, ... }

Flat Access Alternative (for convenience):

// Also export flat structure for backward compatibility and convenience
export const flatQueries = {
  // Core
  currentUser: queries.core.user.current,

  // Sport
  getCompetitionBySport: queries.sport.competition.getBySport,

  // Ballr
  playerSearch: queries.ballr.superCoach.playerSearch,
  playerComparison: queries.ballr.superCoach.playerComparison,

  // ... all queries flattened
};

export const flatCommands = {
  // Core
  createUser: commands.core.user.create,
  updateUser: commands.core.user.update,

  // Sport
  createTeam: commands.sport.team.create,
  createSportingEvent: commands.sport.sportingEvent.create,

  // ... all commands flattened
};

Benefits of Using DataContract Namespaces: 1. Explicit Control - Backend developers explicitly declare client-side structure 2. Decoupled from C# Organization - Can reorganize C# namespaces without breaking clients 3. Part of Serialization Contract - Already used by WCF/DataContractSerializer 4. Stable Public API - Changes to internal structure don't affect client SDK 5. Intentional Design - Forces consideration of client-side developer experience 6. Easy Refactoring - Move queries between domains by changing one attribute

Benefits of Hierarchical Namespacing: 1. Discoverability - Browse related queries/commands by domain 2. IntelliSense - Type ahead through the namespace hierarchy 3. No Collisions - Multiple create commands can coexist 4. Tree Shaking - Import only what you need 5. Logical Grouping - Mirrors backend domain structure 6. Flexibility - Both namespaced and flat access patterns

Example: Reorganizing Without Breaking Clients:

// Before: Query is in Sport module
[DataContract(Name = "PlayerSearch", Namespace = LbsNamespace.SportQueries)]
public class PlayerSearchQuery : IQuery { }

// C# file location: LBS.Domain.Sport/Queries/PlayerSearchQuery.cs
// Client uses: queries.sport.playerSearch({ ... })

// Later: Move to SuperCoach module for better organization
[DataContract(Name = "PlayerSearch", Namespace = LbsNamespace.SuperCoachQueries)]
public class PlayerSearchQuery : IQuery { }

// C# file location: LBS.Ballr.Core/SuperCoach/Queries/PlayerSearchQuery.cs
// Client uses: queries.superCoach.playerSearch({ ... })
// ^ Different C# namespace, client API changed intentionally via attribute

// The DataContract Namespace controls client API, not C# namespace!
// You can move files around in C# without affecting clients if you keep
// the DataContract Namespace the same.

Package Exports Strategy:

// Main entry point: src/index.ts
export { FoundryClient } from './client';
export { queries, commands } from './generated';
export { flatQueries, flatCommands } from './generated/flat';

// Module-specific exports for targeted imports (tree-shakeable)
export * as coreQueries from './generated/queries/core';
export * as sportQueries from './generated/queries/sport';
export * as superCoachQueries from './generated/queries/superCoach';
export * as ballrQueries from './generated/queries/ballr';
export * as discordQueries from './generated/queries/discord';

export * as coreCommands from './generated/commands/core';
export * as sportCommands from './generated/commands/sport';
export * as superCoachCommands from './generated/commands/superCoach';
export * as ballrCommands from './generated/commands/ballr';
export * as discordCommands from './generated/commands/discord';

// Type exports (all generated types)
export * from './types';

// Constants (RoleDefinition, UserStatus, etc.)
export * from './generated/constants';

Advanced Import Patterns:

// Import everything (most common)
import { queries, commands } from '@lbs-foundry/typescript-sdk';
const players = await client.query(queries.superCoach.playerSearch({ ... }));

// Import specific module only (tree-shakeable)
import { superCoachQueries } from '@lbs-foundry/typescript-sdk';
const players = await client.query(superCoachQueries.playerSearch({ ... }));

// Import flat for quick migration
import { flatQueries, flatCommands } from '@lbs-foundry/typescript-sdk';
const players = await client.query(flatQueries.playerSearch({ ... }));

// Mix and match styles
import { queries, flatCommands, superCoachQueries } from '@lbs-foundry/typescript-sdk';

// Import just types (for typing your own functions)
import type { PlayerSearchQuery, CreateUserCommand } from '@lbs-foundry/typescript-sdk';

Visual Comparison - IntelliSense Experience:

// WITHOUT module grouping (flat structure)
import { queries } from '@lbs-foundry/typescript-sdk';

queries. // IntelliSense shows 100+ items:
// playerSearch, currentUser, playerComparison, getCompetitionBySport,
// listContests, getContestById, getGroups, discordVerificationStatus,
// ... overwhelming!

// WITH module grouping (object literal structure)
import { queries } from '@lbs-foundry/typescript-sdk';

queries. // IntelliSense shows 5-7 modules:
// ├─ core         (ModuleDefinition.Core)
// ├─ sport        (ModuleDefinition.SportCore)
// ├─ superCoach   (ModuleDefinition.SuperCoachNrl)
// ├─ ballr        (ModuleDefinition.BallrContests)
// └─ discord      (ModuleDefinition.Discord)

queries.superCoach. // Shows all SuperCoach queries:
// ├─ playerSearch
// ├─ playerComparison
// ├─ playerRoundSearch
// └─ profileBySlug

queries.sport. // Shows all Sport queries:
// ├─ getCompetitionBySport
// ├─ getGroups
// └─ ...

// Clear, organized, aligned with your module structure!

Migration Path:

For teams already using manual types, the SDK provides both patterns:

// Step 1: Install SDK alongside existing code
pnpm add @lbs-foundry/typescript-sdk

// Step 2: Start using flat exports (drop-in replacement)
import { flatQueries, flatCommands } from '@lbs-foundry/typescript-sdk';
const query = flatQueries.playerSearch({ ... }); // Same as before!
const command = flatCommands.createUser({ ... });

// Step 3: Gradually migrate to module-grouped structure
import { queries, commands } from '@lbs-foundry/typescript-sdk';
const query = queries.superCoach.playerSearch({ ... }); // Organized by module
const command = commands.core.createUser({ ... });

// Step 4: Remove manual type definitions
// Delete old manually-defined types, rely on SDK
// Old: src/types/queries.ts DELETE
// Old: src/types/commands.ts DELETE
// New: import from '@lbs-foundry/typescript-sdk' 
  1. Namespace Extraction from DataContract Attribute:
// In the C# generator tool (using Roslyn)

public class NamespaceExtractor
{
    public TypeScriptNamespace ExtractNamespace(INamedTypeSymbol typeSymbol)
    {
        // Get DataContract attribute
        var dataContractAttr = typeSymbol.GetAttributes()
            .FirstOrDefault(a => a.AttributeClass?.Name == "DataContractAttribute");

        if (dataContractAttr == null)
        {
            throw new Exception($"Type {typeSymbol.Name} missing [DataContract] attribute");
        }

        // Extract Name property
        var name = dataContractAttr.NamedArguments
            .FirstOrDefault(arg => arg.Key == "Name")
            .Value.Value?.ToString();

        // Extract Namespace property
        var namespaceValue = dataContractAttr.NamedArguments
            .FirstOrDefault(arg => arg.Key == "Namespace")
            .Value.Value?.ToString();

        if (string.IsNullOrEmpty(namespaceValue))
        {
            // Fallback: use LbsNamespace.Default if not specified
            namespaceValue = "LBS.Foundry";
        }

        return ParseNamespace(namespaceValue, name);
    }

    private TypeScriptNamespace ParseNamespace(string dataContractNamespace, string typeName)
    {
        // Example: "LBS.Foundry.Queries.Ballr.SuperCoach"
        var parts = dataContractNamespace.Split('.');

        if (parts.Length < 3 || parts[0] != "LBS" || parts[1] != "Foundry")
        {
            // Unrecognized format, put in root
            return new TypeScriptNamespace { Path = new[] { "misc" }, Name = typeName };
        }

        // Skip "LBS.Foundry"
        var relevantParts = parts.Skip(2).ToArray();

        if (relevantParts.Length == 0)
        {
            // Just "LBS.Foundry", no category
            return new TypeScriptNamespace { Path = new[] { "root" }, Name = typeName };
        }

        // First part should be "Queries" or "Commands"
        var category = relevantParts[0]; // "Queries" or "Commands"

        if (category != "Queries" && category != "Commands")
        {
            // Unknown category
            return new TypeScriptNamespace { Path = new[] { "misc" }, Name = typeName };
        }

        // Remaining parts form the path: ["Ballr", "SuperCoach"]
        var path = relevantParts.Skip(1)
            .Select(ToCamelCase)
            .ToArray();

        return new TypeScriptNamespace
        {
            Category = category.ToLower(), // "queries" or "commands"
            Path = path, // ["ballr", "superCoach"]
            Name = ToCamelCase(typeName) // "playerSearch"
        };
    }

    private string ToCamelCase(string pascalCase)
    {
        if (string.IsNullOrEmpty(pascalCase)) return pascalCase;
        return char.ToLowerInvariant(pascalCase[0]) + pascalCase.Substring(1);
    }
}

// Usage in generator
var namespaceInfo = extractor.ExtractNamespace(queryType);
// namespaceInfo.Category = "queries"
// namespaceInfo.Path = ["ballr", "superCoach"]
// namespaceInfo.Name = "playerSearch"
// Generated: queries.ballr.superCoach.playerSearch

Fallback Handling:

// If a type has no Namespace specified, use intelligent defaults
if (string.IsNullOrEmpty(dataContractNamespace))
{
    // Inspect C# namespace as fallback
    var csharpNamespace = typeSymbol.ContainingNamespace.ToDisplayString();

    if (csharpNamespace.Contains("LBS.Ballr"))
        return "LBS.Foundry.Queries.Ballr";
    else if (csharpNamespace.Contains("LBS.Domain.Core"))
        return "LBS.Foundry.Queries.Core";
    else if (csharpNamespace.Contains("LBS.Domain.Sport"))
        return "LBS.Foundry.Queries.Sport";
    else if (csharpNamespace.Contains("LBS.Domain.Fantasy"))
        return "LBS.Foundry.Queries.Fantasy";
    else
        return "LBS.Foundry"; // Root level
}
  1. Generate Namespaced Builders (see namespacing strategy above for full structure)

  2. Generate Flat Builders (for backward compatibility):

    // Flat structure - simple object with all builders
    export const flatQueries = {
      // Auto-generated from all query types
      currentUser: queries.core.user.current,
      playerSearch: queries.ballr.superCoach.playerSearch,
      playerComparison: queries.ballr.superCoach.playerComparison,
      getCompetitionBySport: queries.sport.competition.getBySport,
      // ... all queries
    };
    
    export const flatCommands = {
      // Auto-generated from all command types
      createUser: commands.core.user.create,
      updateUser: commands.core.user.update,
      createTeam: commands.sport.team.create,
      updateParticipantSlugName: commands.sport.participant.updateSlugName,
      // ... all commands
    };
    
    // Usage - simple and familiar
    const query = flatQueries.playerSearch({
      teams: ['Panthers'],
      minPrice: 200000
    });
    
    const command = flatCommands.createUser({
      email: 'user@example.com',
      emailVerified: true,
      createdAt: new Date().toISOString(),
      status: UserStatus.Active,
      roles: [RoleDefinition.Member],
      userType: AccountType.Human
    });
    

  3. Add response type inference:

    // Map queries to their response types
    export interface QueryResponseMap {
      PlayerSearch: PlayerSearchResult[];
      CurrentUser: CurrentUserResponse;
      PlayerComparison: PlayerComparisonStatsQueryResultContract[];
      BallrContests: BallrContestContract[];
      GetBallrContestById: BallrContestDetailContract;
      GetCompetitionBySport: CompetitionContract[];
      // ... auto-generated for all queries
    }
    
    // Map commands to their aggregate ID types
    export interface CommandResponseMap {
      CreateUser: string; // UserId
      UpdateUser: string; // UserId
      DeleteUser: string; // UserId
      UpdateParticipantSlugName: string; // ParticipantId
      CreateTeam: string; // TeamId
      CreateSportingEvent: string; // SportingEventId
      CreateCompetition: string; // CompetitionId
      // ... auto-generated for all commands
    }
    
    // Type-safe query execution with inference
    export async function executeQuery<K extends keyof QueryResponseMap>(
      client: FoundryQueryClient,
      query: Extract<Query, { queryType: K }>
    ): Promise<QueryResponseMap[K]> {
      const response = await client.query(query);
      return response.data as QueryResponseMap[K];
    }
    
    // Type-safe command execution with inference
    export async function executeCommand<K extends keyof CommandResponseMap>(
      client: FoundryCommandClient,
      command: Extract<Command, { commandType: K }>
    ): Promise<CommandResponse<CommandResponseMap[K]>> {
      return await client.execute(command);
    }
    
    // Usage with full type inference
    const playerResults = await executeQuery(client, queries.ballr.superCoach.playerSearch({
      minPrice: 200000
    })); // Type: PlayerSearchResult[]
    
    const userId = await executeCommand(client, commands.core.user.create({
      email: 'user@example.com',
      // ...
    })); // Type: CommandResponse<string>
    

  4. Generate TypeScript enums for constants:

    // From C# static classes
    export const RoleDefinition = {
      Admin: 'Admin',
      UserManager: 'UserManager',
      Member: 'Member',
      ServiceAccount: 'ServiceAccount'
    } as const;
    
    export type RoleDefinition = typeof RoleDefinition[keyof typeof RoleDefinition];
    
    export const UserStatus = {
      Active: 'Active',
      Inactive: 'Inactive',
      Suspended: 'Suspended',
      Deleted: 'Deleted'
    } as const;
    
    export type UserStatus = typeof UserStatus[keyof typeof UserStatus];
    
    export const AccountType = {
      Human: 'Human',
      ServiceAccount: 'ServiceAccount'
    } as const;
    
    export type AccountType = typeof AccountType[keyof typeof AccountType];
    

  5. Generate authorization metadata:

    // Extract from [RequiresRoles] attributes
    export const QueryAuthorization = {
      PlayerSearch: { type: 'public' as const, roles: [] },
      CurrentUser: { type: 'secure' as const, roles: [RoleDefinition.Member] },
      PlayerRoundSearch: { type: 'secure' as const, roles: [RoleDefinition.Member] },
      // ... auto-generated for all queries
    } as const;
    
    export const CommandAuthorization = {
      CreateUser: {
        type: 'secure' as const,
        roles: [RoleDefinition.UserManager, RoleDefinition.Admin]
      },
      UpdateParticipantSlugName: {
        type: 'secure' as const,
        roles: [RoleDefinition.Admin]
      },
      DeleteUser: {
        type: 'secure' as const,
        roles: [RoleDefinition.Admin]
      },
      // ... auto-generated for all commands
    } as const;
    
    // Helper to check if user can execute command
    export function canExecuteCommand(
      commandType: keyof typeof CommandAuthorization,
      userRoles: string[]
    ): boolean {
      const authInfo = CommandAuthorization[commandType];
      return authInfo.roles.length === 0 ||
             authInfo.roles.some(role => userRoles.includes(role));
    }
    
    // Usage in React
    const userRoles = useCurrentUser().roles;
    const canCreateUser = canExecuteCommand('CreateUser', userRoles);
    
    <button disabled={!canCreateUser}>
      Create User
    </button>
    

Phase 4: Build Automation (Week 3)

Goal: Integrate with existing CI/CD pipeline

1. Local Development Script:

# tools/generate-typescript-sdk.ps1
param(
    [string]$OutputPath = "./sdk/typescript",
    [switch]$Watch = $false
)

# Build the generator tool
dotnet build src/Tools/LBS.Tools.TypeScriptGenerator/LBS.Tools.TypeScriptGenerator.csproj

# Run type generation
dotnet run --project src/Tools/LBS.Tools.TypeScriptGenerator/LBS.Tools.TypeScriptGenerator.csproj `
    --output $OutputPath `
    --assemblies "src/Domain/*/bin/Debug/net10.0/*.dll" `
    --include-xml-docs

# Build npm package
cd $OutputPath
npm install
npm run build
npm pack

Write-Host "TypeScript SDK generated at $OutputPath"

2. GitHub Actions Workflow (Private GitHub Packages):

# .github/workflows/publish-typescript-sdk.yml
name: Generate and Publish TypeScript SDK

on:
  push:
    branches: [ "main" ]
    paths:
      - 'src/Domain/**/*.cs'
      - 'src/Core/**/*.cs'
      - 'tools/generate-typescript-sdk.ps1'
  workflow_dispatch:

jobs:
  generate-and-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write  # Required for GitHub Packages

    steps:
    - uses: actions/checkout@v4

    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '9.0.x'

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20.x'
        registry-url: 'https://npm.pkg.github.com'
        scope: '@your-github-org'  # Replace with your GitHub org/username

    - name: Generate Version
      id: version
      run: |
        VERSION="1.$(date +'%Y.%m%d.%H%M').${{ github.run_number }}"
        echo "VERSION=$VERSION" >> $GITHUB_ENV

    - name: Build Generator Tool
      run: dotnet build src/Tools/LBS.Tools.TypeScriptGenerator/LBS.Tools.TypeScriptGenerator.csproj

    - name: Generate TypeScript Types
      run: |
        dotnet run --project src/Tools/LBS.Tools.TypeScriptGenerator/LBS.Tools.TypeScriptGenerator.csproj \
          --output ./sdk/typescript \
          --version ${{ env.VERSION }} \
          --include-xml-docs

    - name: Build npm Package
      working-directory: ./sdk/typescript
      run: |
        npm install
        npm run build
        npm run test

    - name: Publish to GitHub Packages
      working-directory: ./sdk/typescript
      run: npm publish
      env:
        NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Automatic GitHub token

    - name: Create GitHub Release
      uses: actions/create-release@v1
      with:
        tag_name: typescript-sdk-v${{ env.VERSION }}
        release_name: TypeScript SDK v${{ env.VERSION }}
        body: |
          Auto-generated TypeScript SDK for LBS Foundry API
          Version: ${{ env.VERSION }}

          ## Installation

          Add to your `.npmrc`:
          ```
          @your-github-org:registry=https://npm.pkg.github.com
          //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
          ```

          Install the package:
          ```bash
          npm install @your-github-org/lbs-foundry-typescript-sdk@${{ env.VERSION }}
          ```

          ## Changes
          - Auto-generated from C# types in commit ${{ github.sha }}
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3. Package.json Template (GitHub Packages):

{
  "name": "@your-github-org/lbs-foundry-typescript-sdk",
  "version": "{{VERSION}}",
  "description": "Auto-generated TypeScript SDK for LBS Foundry API (Private)",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "repository": {
    "type": "git",
    "url": "https://github.com/your-github-org/LBS.Foundry.git"
  },
  "publishConfig": {
    "registry": "https://npm.pkg.github.com"
  },
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "lint": "eslint src/**/*.ts",
    "prepublishOnly": "npm run build && npm test"
  },
  "keywords": ["lbs", "foundry", "sports", "api", "sdk"],
  "author": "LuckBox Studios",
  "license": "UNLICENSED",
  "private": false,
  "dependencies": {
    "axios": "^1.6.0"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "typescript": "^5.3.0",
    "jest": "^29.7.0",
    "@typescript-eslint/eslint-plugin": "^6.13.0",
    "@typescript-eslint/parser": "^6.13.0"
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ]
}

Important Configuration Notes: - name: Must start with @your-github-org/ to match your GitHub organization - publishConfig.registry: Points to GitHub Packages - license: Use "UNLICENSED" for private packages - repository: Links to your GitHub repo (required for GitHub Packages)

Phase 5: Documentation & Examples (Week 3)

Goal: Enable rapid adoption

Tasks: 1. Generate README with usage examples 2. Create example projects: - React app with React Query integration - Node.js service consuming queries and commands - Testing examples with type-safe mocks 3. Add inline JSDoc examples for each query/command 4. Create migration guide from manual types to SDK

Real-World Example - User Management Component:

import { FoundryClient, queries, commands, RoleDefinition, UserStatus, AccountType } from '@lbs-foundry/typescript-sdk';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

const client = new FoundryClient({
  baseUrl: process.env.REACT_APP_API_URL,
  auth: { type: 'bearer', token: authToken }
});

function UserManagementPage() {
  const queryClient = useQueryClient();

  // Query current user with full type safety
  const { data: currentUser } = useQuery({
    queryKey: ['current-user'],
    queryFn: () => client.query(queries.currentUser())
  });

  // Create user mutation with type-safe command
  const createUserMutation = useMutation({
    mutationFn: (userData: {
      email: string;
      firstName: string;
      lastName: string;
    }) => client.execute(
      commands.createUser({
        email: userData.email,
        firstName: userData.firstName,
        lastName: userData.lastName,
        emailVerified: false,
        createdAt: new Date().toISOString(),
        status: UserStatus.Active,
        roles: [RoleDefinition.Member],
        userType: AccountType.Human
      })
    ),
    onSuccess: (response) => {
      console.log('Created user with ID:', response.aggregateRootId);
      queryClient.invalidateQueries(['users']);
    }
  });

  // Check authorization before showing create button
  const canCreateUsers = currentUser?.roles.some(
    role => [RoleDefinition.Admin, RoleDefinition.UserManager].includes(role)
  );

  return (
    <div>
      <h1>User Management</h1>

      {canCreateUsers && (
        <button onClick={() => createUserMutation.mutate({
          email: 'newuser@example.com',
          firstName: 'John',
          lastName: 'Doe'
        })}>
          Create User
        </button>
      )}

      {/* TypeScript knows the exact shape of currentUser */}
      <div>
        Logged in as: {currentUser?.name} ({currentUser?.email})
        Roles: {currentUser?.roles.join(', ')}
      </div>
    </div>
  );
}

Example Documentation:

# @your-github-org/lbs-foundry-typescript-sdk

Auto-generated TypeScript SDK for LBS Foundry API with full type safety (Private Package).

## Installation

### Prerequisites

You need a GitHub Personal Access Token (PAT) with `read:packages` permission.

#### Create GitHub Token

1. Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
2. Click "Generate new token (classic)"
3. Give it a name: "LBS Foundry SDK"
4. Select scopes:
   - `read:packages` (required to download packages)
   - `write:packages` (only if you're publishing)
5. Click "Generate token"
6. **Copy the token immediately** (you won't see it again!)

### Configure npm for GitHub Packages

Create or edit `.npmrc` in your project root (or globally in `~/.npmrc`):

```bash
# .npmrc
@your-github-org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN_HERE

Security: Add .npmrc to .gitignore to avoid committing your token!

Alternative: Use Environment Variable (Recommended for CI/CD):

# .npmrc (safe to commit)
@your-github-org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

Then set the environment variable:

# Local development
export GITHUB_TOKEN=your_token_here

# Or in CI/CD (GitHub Actions)
# Uses ${{ secrets.GITHUB_TOKEN }} automatically

Install the Package

pnpm add @your-github-org/lbs-foundry-typescript-sdk

Verify Installation

import { FoundryClient, queries, commands } from '@your-github-org/lbs-foundry-typescript-sdk';

console.log('SDK installed successfully!');

Quick Start

import { FoundryClient, queries, commands } from '@your-github-org/lbs-foundry-typescript-sdk';

// Initialize client
const client = new FoundryClient({
  baseUrl: 'https://api.ballr.live',
  auth: {
    type: 'bearer',
    token: 'your-jwt-token'
  }
});

// Execute type-safe query with namespaced access
const playerQuery = queries.superCoach.playerSearch({
  teams: ['Panthers', 'Eels'],
  minPrice: 200000,
  positions: ['Halfback']
});

const response = await client.query(playerQuery);
console.log(response.data); // Fully typed as PlayerSearchResult[]

// Execute type-safe command
const result = await client.execute(
  commands.sport.createTeam({
    displayName: 'Panthers',
    // ... other properties
  })
);
console.log('Created team:', result.aggregateRootId);

Using in GitHub Actions (CI/CD)

GitHub Actions automatically provides a GITHUB_TOKEN that can access your organization's packages:

# .github/workflows/build-frontend.yml
name: Build Frontend

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20.x'
        registry-url: 'https://npm.pkg.github.com'
        scope: '@your-github-org'

    - name: Install dependencies
      run: npm install
      env:
        NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Automatic token

    - name: Build
      run: npm run build

    - name: Test
      run: npm test

The GITHUB_TOKEN is automatically provided and has read access to packages in the same organization.

React Query Integration

import { useQuery, useMutation } from '@tanstack/react-query';
import { queries, commands } from '@lbs-foundry/typescript-sdk';

function PlayerList() {
  // Type-safe query
  const { data, isLoading } = useQuery({
    queryKey: ['players', 'search'],
    queryFn: () => foundryClient.query(
      queries.playerSearch({ minPrice: 200000 })
    )
  });

  // Type-safe command mutation
  const updateParticipantMutation = useMutation({
    mutationFn: (params: { participantId: string; slugName: string }) =>
      foundryClient.execute(
        commands.updateParticipantSlugName({
          aggregateRootId: params.participantId,
          slugName: params.slugName
        })
      )
  });

  if (isLoading) return <Loading />;

  return (
    <ul>
      {data?.data.map(player => (
        <li key={player.id}>
          {player.displayName}
          <button onClick={() => updateParticipantMutation.mutate({
            participantId: player.id,
            slugName: player.displayName.toLowerCase().replace(/\s+/g, '-')
          })}>
            Update Slug
          </button>
        </li>
      ))}
    </ul>
  );
}

Available Queries and Commands

The SDK auto-generates all queries and commands from your C# codebase:

Queries (via queries object): - playerSearch - Search for players with filters - currentUser - Get authenticated user info - playerComparison - Compare multiple players - ballrContests - List contests - ... all other queries in your domain

Commands (via commands object): - createUser - Create a new user (requires UserManager or Admin) - updateUser - Update user details (requires UserManager or Admin) - deleteUser - Delete a user (requires Admin) - updateParticipantSlugName - Update participant slug (requires Admin) - createTeam - Create a new team - ... all other commands in your domain

## Technology Choices

### Namespace Source: **DataContract Attribute Properties**

**Key Decision**: Use `[DataContract(Name, Namespace)]` attributes as the source of truth for TypeScript namespace generation.

**Considered Alternatives**:

| Approach | Pros | Cons | Decision |
|----------|------|------|----------|
| **DataContract Namespace** | Explicit, decoupled from C# structure, part of serialization contract | Requires adding namespace to attributes | **Selected** |
| **Parse C# Namespaces** | Automatic, no attribute changes needed | Fragile, couples client API to internal structure, hard to reorganize | Rejected |
| **Separate Config File** | Very flexible | Disconnected from code, easy to get out of sync | Rejected |
| **Naming Conventions** | Simple to implement | Inflexible, error-prone | Rejected |

**Rationale for DataContract Namespaces**:
1. **Explicit API Design**: Backend developers consciously design client-side structure
2. **Decoupled Evolution**: C# refactoring doesn't break client SDK
3. **Single Source of Truth**: Same attribute used for serialization and SDK generation
4. **Type-Safe**: Namespace constants in `LbsNamespace.cs` prevent typos
5. **Discoverable**: All namespace options visible in one place

### Source Generation Approach: **Custom Roslyn-based Generator**

**Considered Alternatives**:

| Approach | Pros | Cons | Decision |
|----------|------|------|----------|
| **NSwag** | Industry standard, OpenAPI integration | Requires OpenAPI spec, doesn't preserve `DataContract` semantics | Rejected |
| **TypeGen** | Established library, attribute-based | Limited customization, no query/command awareness, can't extract DataContract namespace | Rejected |
| **Custom Roslyn Generator** | Full control, preserves domain semantics, can extract attributes, can inject helpers | Development overhead | **Selected** |
| **T4 Templates** | Built into Visual Studio | Outdated, poor tooling, limited attribute access | Rejected |

**Rationale for Custom Generator**:
1. **Domain Awareness**: Understands `IQuery`, `ICommand`, `IPublicQuery`, `ISecureQuery` semantics
2. **Attribute Extraction**: Can read `DataContract` Name and Namespace properties via Roslyn
3. **Exact Mapping**: Preserves `DataContract` names and query type resolution logic
4. **Enhanced Output**: Generates helper functions, builders, and type mappings beyond basic types
5. **Future Extensibility**: Can add WebSocket support, caching metadata, validation rules
6. **Zero Runtime Dependencies**: Pure compile-time generation

### Package Distribution: **GitHub Packages (Private)**

**Registry**: GitHub Packages (private, requires authentication)
**Access**: Private - requires GitHub token for installation
**CI/CD**: GitHub Actions automatic publishing on C# type changes

## Package Architecture Options

### Option 1: Monolithic SDK (Simpler, Recommended for MVP)

**Single package with all modules**:
- **Package**: `@your-github-org/foundry-typescript-sdk`
- **Contains**: Core, Sport, SuperCoach, Ballr, Discord - everything
- **Version**: Single version for entire SDK

**Pros**:
- Simple to maintain - one codebase, one version
- Easy to consume - `npm install` one package
- No dependency coordination needed
- Faster to implement and ship

**Cons**:
- Larger bundle size - includes unused modules
- Cannot update modules independently
- Breaking changes in one module affect all consumers
- Couples unrelated features (Ballr + Discord)

**Usage**:
```typescript
import { FoundryClient, queries, commands } from '@your-github-org/foundry-typescript-sdk';

// All modules available
queries.core.currentUser()
queries.sport.getCompetitionBySport({ ... })
queries.superCoach.playerSearch({ ... })
queries.ballr.listContests({ ... })


Separate packages per module aligned with ModuleDefinition:

@your-github-org/foundry-sdk-core          # Core types + FoundryClient
@your-github-org/foundry-sdk-sport         # Sport module types
@your-github-org/foundry-sdk-supercoach    # SuperCoach module types
@your-github-org/foundry-sdk-ballr         # Ballr module types
@your-github-org/foundry-sdk-discord       # Discord module types
@your-github-org/foundry-sdk               # Meta-package (installs all)

Package Structure:

Core Package (required, contains client):

// @your-github-org/foundry-sdk-core/package.json
{
  "name": "@your-github-org/foundry-sdk-core",
  "version": "1.2025.0123.1030.42",
  "dependencies": {
    "axios": "^1.6.0"
  }
}

Contains: - FoundryClient class (query/command executors) - Core module queries/commands (queries.core.*, commands.core.*) - Shared types (RoleDefinition, UserStatus, etc.) - Base infrastructure

Feature Packages (optional, depend on core):

// @your-github-org/foundry-sdk-sport/package.json
{
  "name": "@your-github-org/foundry-sdk-sport",
  "version": "1.2025.0123.1030.42",
  "peerDependencies": {
    "@your-github-org/foundry-sdk-core": "^1.0.0"
  }
}

Contains: - Sport module queries/commands (queries.sport.*, commands.sport.*) - Sport-specific types (TeamContract, CompetitionContract, etc.)

// @your-github-org/foundry-sdk-ballr/package.json
{
  "name": "@your-github-org/foundry-sdk-ballr",
  "version": "2.2025.0125.1400.15",  // ← Different version!
  "peerDependencies": {
    "@your-github-org/foundry-sdk-core": "^1.0.0"
  }
}

Meta Package (convenience, installs all):

// @your-github-org/foundry-sdk/package.json
{
  "name": "@your-github-org/foundry-sdk",
  "version": "1.2025.0123.1030.42",
  "dependencies": {
    "@your-github-org/foundry-sdk-core": "^1.0.0",
    "@your-github-org/foundry-sdk-sport": "^1.0.0",
    "@your-github-org/foundry-sdk-supercoach": "^1.0.0",
    "@your-github-org/foundry-sdk-ballr": "^2.0.0",
    "@your-github-org/foundry-sdk-discord": "^1.0.0"
  }
}

Pros: - Smaller bundles - Only install what you need - Independent versioning - Ballr can be v2, Sport can be v1 - Tree-shakeable - Better for web apps (bundle size matters) - Team autonomy - Ballr team updates their package independently - Breaking changes isolated - Sport breaking change doesn't affect Ballr consumers - Clearer dependencies - Know exactly what you're using

Cons: - More complex to maintain - multiple packages, versions - Dependency coordination - ensure compatible peer dependencies - More CI/CD jobs - publish multiple packages - Initial setup overhead - more configuration

Usage - Install Only What You Need:

# Ballr frontend - only needs Ballr module
pnpm add @your-github-org/foundry-sdk-core
pnpm add @your-github-org/foundry-sdk-ballr

# Sport admin dashboard - only needs Sport module
pnpm add @your-github-org/foundry-sdk-core
pnpm add @your-github-org/foundry-sdk-sport

# Full app - needs everything
pnpm add @your-github-org/foundry-sdk  # Installs all modules

Usage - Import Only What You Need:

// Ballr app - only imports Ballr types
import { FoundryClient } from '@your-github-org/foundry-sdk-core';
import { queries as ballrQueries, commands as ballrCommands } from '@your-github-org/foundry-sdk-ballr';

const client = new FoundryClient({ ... });
const contests = await client.query(ballrQueries.listContests({ ... }));

// No Sport/Discord types in bundle! 

Generated File Structure (Modular):

packages/
├── core/                              # @your-github-org/foundry-sdk-core
│   ├── src/
│   │   ├── client/
│   │   │   ├── FoundryClient.ts       # Main client
│   │   │   ├── FoundryQueryClient.ts
│   │   │   └── FoundryCommandClient.ts
│   │   ├── generated/
│   │   │   ├── types/
│   │   │   │   ├── queries.ts         # Core queries only
│   │   │   │   ├── commands.ts        # Core commands only
│   │   │   │   └── responses.ts
│   │   │   ├── queries.ts             # queries.core.*
│   │   │   ├── commands.ts            # commands.core.*
│   │   │   └── constants.ts           # Shared constants
│   │   └── index.ts
│   └── package.json
├── sport/                             # @your-github-org/foundry-sdk-sport
│   ├── src/
│   │   ├── generated/
│   │   │   ├── types/
│   │   │   │   ├── queries.ts         # Sport queries
│   │   │   │   ├── commands.ts        # Sport commands
│   │   │   │   └── responses.ts
│   │   │   ├── queries.ts             # queries.sport.*
│   │   │   └── commands.ts            # commands.sport.*
│   │   └── index.ts
│   └── package.json
├── ballr/                             # @your-github-org/foundry-sdk-ballr
│   ├── src/
│   │   ├── generated/
│   │   │   ├── types/
│   │   │   │   ├── queries.ts         # Ballr queries
│   │   │   │   ├── commands.ts        # Ballr commands
│   │   │   │   └── responses.ts
│   │   │   ├── queries.ts             # queries.ballr.*
│   │   │   └── commands.ts            # commands.ballr.*
│   │   └── index.ts
│   └── package.json
├── supercoach/                        # @your-github-org/foundry-sdk-supercoach
│   └── ...
├── discord/                           # @your-github-org/foundry-sdk-discord
│   └── ...
└── foundry-sdk/                       # @your-github-org/foundry-sdk (meta)
    ├── src/
    │   └── index.ts                   # Re-exports all packages
    └── package.json                   # Depends on all modules


Start monolithic, migrate to modular as needed:

Phase 1: Ship monolithic SDK - Single package with all modules - Get to production quickly - Learn usage patterns

Phase 2: Extract high-change modules - Keep core monolithic - Split out Ballr if it changes frequently - Split out experimental modules

Phase 3: Full modular if needed - Migrate to fully modular only if bundle size or versioning becomes a pain


Key Insight

The client is the same, only types differ!

  • FoundryClient → Same for everyone
  • client.query() → Same API
  • client.execute() → Same API
  • Types (queries, commands, responses) → Only difference

Recommendation: Single Package with Tree-Shaking

Package: @your-github-org/foundry-typescript-sdk

Structure:

// Single package, organized by module
import { FoundryClient } from '@your-github-org/foundry-typescript-sdk';

// Import only the types you need - modern bundlers tree-shake the rest
import { queries } from '@your-github-org/foundry-typescript-sdk/ballr';
import { queries as sportQueries } from '@your-github-org/foundry-typescript-sdk/sport';

// Or import everything (simpler, slightly larger bundle)
import { queries, commands } from '@your-github-org/foundry-typescript-sdk';

Package Export Structure (package.json):

{
  "name": "@your-github-org/foundry-typescript-sdk",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./core": {
      "types": "./dist/modules/core/index.d.ts",
      "import": "./dist/modules/core/index.js"
    },
    "./ballr": {
      "types": "./dist/modules/ballr/index.d.ts",
      "import": "./dist/modules/ballr/index.js"
    },
    "./sport": {
      "types": "./dist/modules/sport/index.d.ts",
      "import": "./dist/modules/sport/index.js"
    },
    "./supercoach": {
      "types": "./dist/modules/supercoach/index.d.ts",
      "import": "./dist/modules/supercoach/index.js"
    }
  }
}

File Structure:

sdk/typescript/
├── src/
│   ├── client/
│   │   ├── FoundryClient.ts          # Single client for everyone
│   │   ├── FoundryQueryClient.ts
│   │   └── FoundryCommandClient.ts
│   ├── modules/
│   │   ├── core/
│   │   │   ├── index.ts              # Export: queries.core.*, commands.core.*
│   │   │   └── types/                # Core types only
│   │   ├── ballr/
│   │   │   ├── index.ts              # Export: queries.ballr.*, commands.ballr.*
│   │   │   └── types/                # Ballr types only
│   │   ├── sport/
│   │   │   ├── index.ts              # Export: queries.sport.*, commands.sport.*
│   │   │   └── types/                # Sport types only
│   │   └── supercoach/
│   │       ├── index.ts              # Export: queries.supercoach.*
│   │       └── types/                # SuperCoach types only
│   └── index.ts                      # Main export - all modules
└── package.json

Usage - Import Only What You Need:

// Ballr app - only import Ballr types (tree-shaken automatically!)
import { FoundryClient } from '@your-github-org/foundry-typescript-sdk';
import { queries, commands } from '@your-github-org/foundry-typescript-sdk/ballr';

const client = new FoundryClient({ ... });
const contests = await client.query(queries.listContests({ ... }));

// Webpack/Vite will remove Sport/SuperCoach types from bundle 

Usage - Import Everything (Simpler):

// Full app - import all types
import { FoundryClient, queries, commands } from '@your-github-org/foundry-typescript-sdk';

const client = new FoundryClient({ ... });
await client.query(queries.ballr.listContests({ ... }));
await client.query(queries.sport.getCompetitionBySport({ ... }));

Why This is Better

Pros: - One package - Simple to maintain and version - One client - No duplication, no confusion - Tree-shakeable - Bundlers remove unused types automatically - Flexible imports - Import per-module or everything - Same API - client.query() / client.execute() for everyone - Fast to ship - No multi-package coordination

When to Split Packages (only if these happen): - Ballr team wants to publish their SDK publicly to npm (separate product) - Version Ballr and Sport independently (major v2 for one, v1 for other) - Tree-shaking isn't working (bundle still too large despite subpath imports)

Bottom Line

Don't overcomplicate it. One package, organized by module, with subpath imports. Modern bundlers handle the rest.

// That's it - simple!
import { FoundryClient } from '@your-github-org/foundry-typescript-sdk';
import { queries } from '@your-github-org/foundry-typescript-sdk/ballr';

Python SDK Support (Phase 4)

Following the successful implementation of the TypeScript SDK, the generator was extended to support multiple languages with Python as the second target language.

Architecture Changes

The generator was refactored to support language abstraction:

Before (TypeScript-only):

LBS.Tools.TypeScriptGenerator/
├── TypeScanner.cs          # Hardcoded TypeScript mapping
├── TypeMapper.cs
├── TypeScriptGenerator.cs
└── Program.cs

After (Multi-language):

LBS.Tools.SdkGenerator/
├── ICodeGenerator.cs          # Language abstraction interface
├── TypeScanner.cs             # Language-agnostic TypeInfo
├── TypeMapper.cs              # TypeScript implementation
├── TypeScriptGenerator.cs     # Implements ICodeGenerator
├── PythonTypeMapper.cs        # Python type mapping
├── PythonGenerator.cs         # Implements ICodeGenerator
└── Program.cs                 # --language parameter

Language Abstraction Interface

public interface ICodeGenerator
{
    GeneratedOutput Generate(List<ScannedType> types);
    string FileExtension { get; }  // ".ts" or ".py"
    string LanguageName { get; }   // "TypeScript" or "Python"
}

Python Type Mapping

C# Type Python Type
string str
int, long int
decimal, double float
bool bool
DateTime datetime
Guid str
List<T> list[T]
Dictionary<K,V> dict[K, V]
T? (nullable) T \| None

Python Code Generation

Generated Dataclass:

from dataclasses import dataclass

@dataclass
class PlayerSearch:
    """Auto-generated dataclass."""
    query_type: str = 'PlayerSearch'
    teams: list[str] | None
    search_term: str | None
    min_price: int
    max_price: int
    positions: list[str] | None
    players_to_include_in_results: list[str]

Generated Builder Function:

def player_search(
    teams: list[str] | None,
    search_term: str | None,
    min_price: int,
    max_price: int,
    positions: list[str] | None,
    players_to_include_in_results: list[str]
) -> PlayerSearch:
    """Create a PlayerSearch instance."""
    return PlayerSearch(
        query_type='PlayerSearch',
        teams=teams,
        search_term=search_term,
        min_price=min_price,
        max_price=max_price,
        positions=positions,
        players_to_include_in_results=players_to_include_in_results,
    )

Python Client Library

Features: - Async/await using httpx - Retry logic with exponential backoff - Generic type hints for IDE support - Full type safety with Python 3.11+ - Modern pyproject.toml packaging

Example Usage:

import asyncio
from foundry_sdk import FoundryClient, FoundryClientConfig
from foundry_sdk.generated.modules.super_coach.queries import player_search

async def main():
    client = FoundryClient(FoundryClientConfig(
        base_url="https://api.foundry.com",
        auth_token="your-bearer-token"
    ))

    result = await client.query(player_search(
        search_term="Smith",
        min_price=100000,
        max_price=500000,
        teams=None,
        positions=None,
        players_to_include_in_results=[]
    ))

    if result.success:
        print(f"Found {len(result.data)} players")

asyncio.run(main())

Python Package Structure

sdk/python/
├── pyproject.toml                  # Modern Python packaging
├── README.md                        # Comprehensive docs
└── src/foundry_sdk/
    ├── __init__.py                 # Main exports
    ├── types.py                    # Core types
    ├── client.py                   # FoundryClient
    ├── query_client.py             # Query execution
    ├── command_client.py           # Command execution
    ├── py.typed                    # PEP 561 marker
    └── generated/                  # Auto-generated
        ├── types.py
        ├── __init__.py
        └── modules/
            ├── core/
            ├── sport/
            ├── super_coach/        # snake_case naming
            ├── ballr/
            └── discord/

CLI Usage

# Generate TypeScript SDK
dotnet run --project src/Tools/LBS.Tools.SdkGenerator -- \
  <assembly-dir> <reference-dir> <output-dir> --language typescript

# Generate Python SDK
dotnet run --project src/Tools/LBS.Tools.SdkGenerator -- \
  <assembly-dir> <reference-dir> <output-dir> --language python

Convenience Scripts:

# Windows
.\scripts\generate-typescript-sdk.ps1
.\scripts\generate-python-sdk.ps1

# Linux/macOS
./scripts/generate-typescript-sdk.sh
./scripts/generate-python-sdk.sh

Benefits of Multi-Language Support

  • Consistent API - Same builder patterns across TypeScript and Python
  • Shared Infrastructure - Single codebase for type scanning
  • Easy Extension - Add new languages by implementing ICodeGenerator
  • Maintained in Sync - Both SDKs generated from same source of truth
  • Use Case Coverage - TypeScript for web, Python for scripts/data science

Adding New Languages

The architecture makes it straightforward to add support for other languages:

  1. Create {Language}TypeMapper.cs - Map C# types to target language
  2. Create {Language}Generator.cs - Implement ICodeGenerator
  3. Register in Program.cs switch statement
  4. Create generation script

Example languages that could be easily added: - C# Client SDK - For external .NET applications - Go - For backend microservices - Rust - For high-performance clients - Java/Kotlin - For Android applications

Import API Support (Phase 5)

The SDK now includes support for the Import API (POST /api/import), enabling external systems to execute bulk data imports through type-safe importers.

What are Importers?

Importers are specialized handlers that convert external data into domain commands for transactional execution. They follow the pattern:

public interface IImporter<TInput>
{
    Task<IEnumerable<IDomainCommand>> ExecuteAsync(TInput input);
}

Example C# Importer:

public sealed class ModelOutputImporter : IImporter<ModelOutputImporterContract>
{
    public async Task<IEnumerable<IDomainCommand>> ExecuteAsync(ModelOutputImporterContract input)
    {
        var commands = new List<IDomainCommand>();

        foreach (var projection in input.PlayerProjections)
        {
            var command = new SetModelProjectionCommand
            {
                AggregateRootId = aggregateId,
                ParticipantId = participantId,
                Round = input.Round,
                Points = projection.ExpectedPoints,
                // ...
            };
            commands.Add(command);
        }

        return commands;
    }
}

Available Importers

Importer Module Purpose Use Case
ClerkWebhookImporter Core User provisioning webhooks Clerk authentication integration
ModelOutputImporter SuperCoach ML model predictions Data science teams pushing player projections
SuperCoachImporter SuperCoach SuperCoach data sync External systems importing fantasy stats
SuperCoachLeaderBoardImporter SuperCoach Contest leaderboards Fantasy league integrations
FoxSportsImporter Sport Fox Sports feeds Data integration from Fox Sports API

SDK Integration

The SDK generates import builders for each importer contract, providing type-safe construction with IntelliSense support.

TypeScript Example:

import { FoundryClient } from '@luckboxstudios/foundry-sdk';
import { imports } from '@luckboxstudios/foundry-sdk/supercoach';

const client = new FoundryClient({
  baseUrl: 'https://api.foundry.com',
  authToken: 'your-bearer-token'
});

// Use generated import builder for type safety
const result = await client.import(
  'ModelOutput',  // Importer type (from ImporterTypeRegistry)
  imports.modelOutput({
    seasonYear: 2025,
    round: 15,
    playerProjections: [
      {
        foxFeedId: '12345',
        expectedPoints: 45.2,
        expectedMinutes: 65.5,
        expectedPrice: 350000
      }
    ]
  })
);

console.log(`Executed ${result.commandsExecuted} commands`);
console.log(`Generated ${result.eventsGenerated} events`);
console.log(`Processed ${result.aggregatesProcessed} aggregates`);

Python Example:

from foundry_sdk import FoundryClient, FoundryClientConfig
from foundry_sdk.supercoach import imports

client = FoundryClient(FoundryClientConfig(
    base_url='https://api.foundry.com',
    auth_token='your-bearer-token'
))

# Use generated import builder for type safety
result = await client.import_data(
    'ModelOutput',  # Importer type (from ImporterTypeRegistry)
    imports.model_output_import(
        season_year=2025,
        round=15,
        player_projections=[
            {
                'fox_feed_id': '12345',
                'expected_points': 45.2,
                'expected_minutes': 65.5,
                'expected_price': 350000
            }
        ]
    )
)

print(f"Success: {result.success}")
print(f"Commands executed: {result.commands_executed}")
print(f"Events generated: {result.events_generated}")

Import API Response

interface ImportExecutionResult {
  requestId: string;
  importId: string;
  success: boolean;
  importerType: string;
  commandsExecuted: number;
  eventsGenerated: number;
  aggregatesProcessed: number;
  executionDuration: string;
  errorMessage?: string;
  errorCode?: string;
}

Transactional Behavior

Importers automatically batch commands by aggregate root and execute in separate transactions:

// These projections will be processed in separate transactions per player
const result = await client.import({
  importerType: 'ModelOutput',
  importData: {
    playerProjections: [
      { foxFeedId: '001', ... },  // Transaction 1 (Player 001)
      { foxFeedId: '001', ... },  // Transaction 1 (Same player)
      { foxFeedId: '002', ... },  // Transaction 2 (Player 002)
      { foxFeedId: '003', ... }   // Transaction 3 (Player 003)
    ]
  }
});

Failure Handling: - Each aggregate processes in its own transaction - Failed aggregates rollback independently - Successful aggregates remain committed - Detailed error reporting per aggregate

Benefits for External Systems

Data Science Teams: - Push ML predictions via ModelOutputImporter - Type-safe contracts for prediction schemas - Automated retry logic with exponential backoff

Integration Pipelines: - Sync external data sources (Fox Sports, NRL) - Bulk operations with transactional guarantees - Progress tracking via import execution IDs

Administrative Operations: - Bulk metadata updates (slug names, URLs) - Seeding/migration operations - Testing data provisioning

Authentication & Authorization

Import API requires same authentication as commands: - Human Users: Clerk JWT Bearer tokens - Service Accounts: Basic Auth (email/password) - Role-Based Access: Importers respect [RequiresRoles] attributes

// Service account authentication
const client = new FoundryClient({
  baseUrl: 'https://api.foundry.com',
  authType: 'basic',
  email: 'service@example.com',
  password: 'secure-password'
});

// Execute privileged import
await client.import({
  importerType: 'ModelOutput',  // Requires ServiceAccount role
  importData: { ... }
});

Generated Import Builders

The SDK automatically generates import builders for each importer contract, organized by module:

TypeScript Structure:

// Generated: sdk/typescript/generated/supercoach/imports.ts
export const imports = {
  // Function name derived from type name (removes "Import" suffix)
  modelOutput: (data: ModelOutputImport): ModelOutputImport => data,
  superCoach: (data: SuperCoachImport): SuperCoachImport => data,
  superCoachLeaderBoard: (data: SuperCoachLeaderBoardImport): SuperCoachLeaderBoardImport => data,
} as const;

Python Structure:

# Generated: sdk/python/generated/supercoach/imports.py
def model_output_import(
    season_year: int,
    round: int,
    player_projections: list
) -> ModelOutputImport:
    return ModelOutputImport(
        season_year=season_year,
        round=round,
        player_projections=player_projections
    )

Key Characteristics: - Module Organization: Import builders grouped by namespace (Core, Sport, SuperCoach, Ballr, Discord) - Naming Convention: Function names derived from type names (e.g., ModelOutputImportmodelOutput() in TypeScript, model_output_import() in Python) - Type Safety: TypeScript uses identity functions for type checking; Python uses dataclass constructors - No Discriminator: Unlike queries/commands, imports don't need a queryType field (wrapped by API in { importerType, importData } payload)

Available Importers: - Core: clerkWebhookImport - Clerk user provisioning webhooks - SuperCoach: modelOutputImport, superCoachImport, superCoachLeaderBoardImport - Fantasy sports data - Sport: foxSportsImport - Sports data feeds - Additional importers registered via ImporterTypeRegistry

Design Decisions

Include All Importers in SDK: Authentication handles access control, no need for filtering

Separate Module Organization: Importers exposed via dedicated namespace/module

Type-Safe Contracts: Importer input types generated alongside queries/commands

Unified Client API: Same client handles queries, commands, and imports

Consequences

Positive

Type Safety: Compile-time validation of all API calls, catch errors before runtime

Developer Productivity: IntelliSense, autocomplete, instant documentation in IDE

Reduced Bugs: Eliminate entire class of runtime errors from API misuse

API Discoverability: Developers can explore available queries/commands via IDE

Automatic Updates: SDK regenerates on every C# change, always in sync

Better Testing: Type-safe mocks, shared contracts between frontend and backend tests

Documentation: Generated types serve as self-documenting API reference

Consistency: Single source of truth (C# types) eliminates drift

Migration Path: Can gradually migrate existing code to use SDK

Private Distribution: GitHub Packages keeps SDK private to your organization

No npm Account Required: Uses GitHub authentication (already have it)

Free for Private Repos: GitHub Packages is free for private repositories

Access Control: Leverages GitHub's existing team/org permissions

Negative

Build Complexity: Additional build step and tooling to maintain

Generation Time: Adds ~30-60 seconds to build pipeline

Breaking Changes: C# type changes automatically propagate to frontend (requires coordination)

Package Management: Teams must update npm package when API changes

Learning Curve: Developers need to understand generated SDK structure

Debugging: Generated code can be harder to debug than hand-written code

Custom Code: Difficult to extend generated types with custom behavior

Token Management: Developers need GitHub Personal Access Tokens configured

Initial Setup: New team members must configure .npmrc with authentication

CI/CD Configuration: Each CI/CD environment needs GitHub token setup

No Public Access: Cannot share SDK with external developers/partners without GitHub access

Known Issues

Nested Type Generation (Critical): Currently, all complex nested types are mapped to any instead of generating proper TypeScript interfaces.

Current Behavior:

export interface CreateLineup {
  queryType: 'CreateLineup';
  sportingEventId: string;
  participants: any[];      // Should be LineupParticipant[]
  dataSource: any;          // Should be DataSource
  sourceId: string | null;
}

Expected Behavior:

export interface DataSource {
  id: string;
  name: string;
  type: string;
}

export interface LineupParticipant {
  participantId: string;
  position: string;
  jerseyNumber: number;
}

export interface CreateLineup {
  queryType: 'CreateLineup';
  sportingEventId: string;
  participants: LineupParticipant[];  // Properly typed
  dataSource: DataSource;             // Properly typed
  sourceId: string | null;
}

Root Cause: The TypeMapper currently only maps primitive types and collections. When encountering complex types (classes/structs), it defaults to any:

// TypeMapper.cs (lines 92-101)
var simpleName = typeInfo.FullTypeName.Split('.').Last().Split('`').First();

if (simpleName.EndsWith("Id"))
{
    return "string";  // Handle strongly-typed IDs
}

return "any";  // All other complex types become 'any'

Impact: - Loss of Type Safety: Defeats primary purpose of generated SDK - No IntelliSense: IDE cannot provide autocomplete for nested objects - Runtime Errors: Type mismatches only caught at runtime, not compile-time - Poor Developer Experience: Must reference C# code to understand structure - Import API Blocked: Cannot generate importer contracts without nested type support

Status: RESOLVED

This issue has been resolved by implementing proper nested type generation: - All complex types referenced by queries, commands, and imports are now recursively scanned - TypeScript interfaces are generated for all nested types with [DataContract] attributes - Python dataclasses are generated with proper type annotations - Import API support is now fully functional with type-safe nested contracts

Examples of Affected Types: - CreateLineup.participantsany[] instead of LineupParticipant[] - CreateCompetition.dataSourceany instead of DataSource - ModelOutputImporterContract.playerProjectionsany[] instead of NrlSuperCoachModelPlayerOutputContract[] - All Dictionary<K,V> with complex value types → Record<K, any>

Required Solution: 1. Dependency Scanning: Recursively scan all referenced types in properties 2. Type Generation: Generate TypeScript interfaces for all complex types (not just queries/commands) 3. Circular Reference Handling: Detect and handle type cycles (A → B → A) 4. Type Mapping Update: Use generated type names instead of any

Workaround (Temporary): Developers must manually define nested types:

// Manual type definitions required
interface LineupParticipant {
  participantId: string;
  position: string;
  jerseyNumber: number;
}

// Use assertion to add type safety
const lineup: CreateLineup = {
  queryType: 'CreateLineup',
  participants: [...] as LineupParticipant[]
};

Priority: HIGH - Blocks Phase 5 (Import API support) and significantly degrades SDK value

Risks and Mitigations

Risk Probability Impact Mitigation
Breaking Changes in Generated Types High High Semantic versioning with major version bumps; deprecation warnings
Build Pipeline Failures Medium Medium Comprehensive tests; fallback to previous SDK version; monitoring
Type Mapping Errors Low High Extensive test suite comparing C# serialization to TypeScript types
GitHub Packages Downtime Low Medium Cache packages in CI/CD; local npm cache; documented recovery process
Generator Tool Bugs Medium High Thorough testing; manual review of generated output; versioned tool
C# API Changes Not Reflected Low High CI/CD blocks merge if generation fails; automated PR checks
Lost GitHub Tokens Medium Medium Document token rotation process; use org-level tokens; store in secrets manager
New Developer Onboarding Issues High Low Clear documentation; automated setup script; onboarding checklist
Token Permission Issues Medium Medium Use minimal required scopes (read:packages); document exact requirements

Success Metrics

Technical KPIs

  • Type Coverage: 100% of public queries/commands have TypeScript equivalents
  • Build Time: SDK generation completes in < 60 seconds
  • Package Size: Generated SDK < 500KB (minified + gzipped)
  • Type Accuracy: Zero runtime type mismatches in production
  • Update Frequency: SDK published within 5 minutes of C# changes merging

Developer KPIs

  • Adoption Rate: 90% of API calls use SDK within 3 months
  • Bug Reduction: 50% reduction in API-related frontend bugs
  • Development Speed: 30% faster feature development using type-safe APIs
  • Developer Satisfaction: Positive feedback on IntelliSense and type safety

Implementation Checklist

Week 1: Foundation

  • [ ] Create LBS.Tools.TypeScriptGenerator project
  • [ ] Implement Roslyn-based assembly scanner
  • [ ] Scan for IQuery, ISecureQuery, IPublicQuery implementations
  • [ ] Scan for DomainCommand<T> implementations
  • [ ] Extract [DataContract(Name, Namespace)] attributes from types
  • [ ] Parse DataContract Namespace to build TypeScript namespace hierarchy
  • [ ] Build C# to TypeScript type mapper
  • [ ] Generate TypeScript interfaces for queries
  • [ ] Generate TypeScript interfaces for commands
  • [ ] Extract [RequiresRoles] attributes for authorization metadata
  • [ ] Add XML doc to JSDoc conversion
  • [ ] Create test suite for type generation
  • [ ] Add fallback logic for types without explicit Namespace

Week 2: Client Library & Namespacing

  • [ ] Expand LbsNamespace.cs with client-side namespace constants
  • [ ] Apply namespace attributes to all queries and commands
  • [ ] Implement FoundryClient base class
  • [ ] Create FoundryQueryClient with query execution methods
  • [ ] Create FoundryCommandClient with command execution methods
  • [ ] Add authentication support (Bearer + Basic)
  • [ ] Implement error handling and retries
  • [ ] Generate hierarchical namespace structure from DataContract Namespace (queries.ballr.superCoach.*)
  • [ ] Generate namespaced query builder functions
  • [ ] Generate namespaced command builder functions
  • [ ] Generate flat query builders for backward compatibility
  • [ ] Generate flat command builders for backward compatibility
  • [ ] Generate query-to-response type mappings
  • [ ] Generate command-to-aggregate type mappings
  • [ ] Generate authorization metadata objects
  • [ ] Create authorization helper functions
  • [ ] Create domain-specific export modules for tree-shaking
  • [ ] Test namespace generation with various DataContract configurations

Week 3: Automation & Distribution

  • [ ] Create PowerShell script for local generation
  • [ ] Set up GitHub Actions workflow
  • [ ] Configure npm publishing with credentials
  • [ ] Create package.json template with metadata
  • [ ] Add versioning strategy
  • [ ] Write comprehensive README
  • [ ] Create example projects
  • [ ] Document migration from manual types

Week 4: Testing & Refinement

  • [ ] Test generation with all existing queries/commands
  • [ ] Validate type accuracy with integration tests
  • [ ] Perform end-to-end testing in React app
  • [ ] Gather team feedback
  • [ ] Refine developer experience
  • [ ] Create troubleshooting guide
  • [ ] Plan ongoing maintenance

References

Internal Documentation

External Resources

Review and Approval

  • Technical Lead: [Name] - Architecture review and feasibility assessment
  • Frontend Lead: [Name] - Developer experience and integration review
  • Backend Lead: [Name] - Type generation accuracy and maintenance review
  • DevOps Lead: [Name] - CI/CD integration and automation review

Date: January 2025 Review Date: February 2025 Status: Awaiting stakeholder review and feasibility validation