😸

信頼出来ないAIコーディング対策としての契約プログラミング

に公開

1. モチベーション

AIコーディングツールは驚異的なスピードでコードを生成しますが、以下のような問題があります:

  • 品質のバラつき: コードは動くが、セキュリティ(認可漏れ)やエラーハンドリングを無視
  • 規約違反: フレームワークの責務分離を無視(Business LayerでDBアクセス等)
  • 認可の混乱: どのレイヤーで何をチェックすべきかAIが理解していない
  • スケーラビリティ: 品質管理が困難で、技術的負債が蓄積

従来の解決策はAIコーディングでは機能しません:

そこで、デコレーター契約プログラミングを用いて、AIが生成するコードに自動的にセーフティネットを適用するゼロトラストアーキテクチャを構築します。

2. 設計思想

核心となる3つの原則

  • PRレビューに期待しない: デコレーターによる契約で自動検証
  • レイヤーごとの責務を強制: 各レイヤーが適切な契約のみを持つ
  • 壊れても露出しない: 契約違反は適切なレイヤーでフォールバック

レイヤーごとの契約責務

  • Presentation Layer: 基本認証(ログイン状態、セッション有効性)
  • Action Layer: アクション権限(ロール、レート制限、入力検証)
  • Business Layer: リソース所有権(自分のデータか、ビジネスルール)
  • Data Layer: データアクセス権限(クエリレベルの最終防御、監査)

AIに対する期待

  • 何かしらの契約を書かなければ壊れるし、そこの部分のチェックが容易
  • コピペみたくなると思うので、AIでも問題なくかける
  • レイヤーごとの責務と契約さえ保てば、AI生成物へのクオリティコントロールとフェイルセーフがある程度できるだろうとの期待

アーキテクチャ図

3. デコレーター契約システム

基本デコレーターの実装

// 契約条件の型定義
type ContractCondition = (input: any, context: any) => boolean | Promise<boolean>;
type ContractValidator = (input: unknown) => any;
type ContractInvariant = (input: any, output: any) => boolean;

interface ContractOptions {
  requires?: Array<ContractCondition | ContractValidator>;
  ensures?: Array<ContractCondition | ((output: any, input: any, context: any) => boolean)>;
  invariants?: Array<ContractInvariant>;
  layer?: 'presentation' | 'action' | 'business' | 'data';
}

// メインの契約デコレーター
function contract(options: ContractOptions) {
  return function(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args: any[]) {
      const [input, context] = args;
      const contractName = `${target.constructor.name}.${propertyName}`;
      
      try {
        // 事前条件チェック(requires)
        let validatedInput = input;
        for (const condition of options.requires || []) {
          if (typeof condition === 'function' && condition.length === 1) {
            // バリデーター(入力変換)
            validatedInput = condition(validatedInput);
          } else {
            // 条件チェック
            await condition(validatedInput, context);
          }
        }
        
        // 元の処理実行
        const result = await originalMethod.call(this, validatedInput, context);
        
        // 事後条件チェック(ensures)
        for (const condition of options.ensures || []) {
          await condition(result, validatedInput, context);
        }
        
        // 不変条件チェック(invariants)
        for (const invariant of options.invariants || []) {
          if (!invariant(validatedInput, result)) {
            throw new ContractError('INVARIANT_VIOLATION', 
              `Invariant condition failed in ${contractName}`
            );
          }
        }
        
        return result;
        
      } catch (error) {
        throw new ContractViolationError(
          contractName, 
          options.layer || 'unknown', 
          error
        );
      }
    };
    
    return descriptor;
  };
}

// 契約エラークラス
class ContractError extends Error {
  constructor(public type: string, message: string) {
    super(message);
    this.name = 'ContractError';
  }
}

class ContractViolationError extends Error {
  constructor(
    public contractName: string,
    public layer: string,
    public originalError: any
  ) {
    super(`Contract violation in ${layer}.${contractName}: ${originalError.message}`);
    this.name = 'ContractViolationError';
  }
  
  getAppropriateResponse() {
    switch (this.layer) {
      case 'presentation':
        return { redirect: '/login', error: 'Authentication required' };
      case 'action':
        return { success: false, error: this.originalError.message };
      case 'business':
        return { success: false, error: 'Permission denied' };
      case 'data':
        return { success: false, error: 'Operation failed' };
      default:
        return { success: false, error: 'An error occurred' };
    }
  }
}

契約条件の実装

// 認証条件
function auth(requiredRole?: string) {
  return async (input: any, context: AuthContext): Promise<boolean> => {
    if (!context.user) {
      throw new ContractError('AUTHENTICATION_REQUIRED', 'User must be logged in');
    }
    
    if (!context.session || new Date(context.session.expiresAt) < new Date()) {
      throw new ContractError('SESSION_EXPIRED', 'Session has expired');
    }
    
    if (requiredRole && !context.user.roles.includes(requiredRole)) {
      throw new ContractError('INSUFFICIENT_ROLE', 
        `Required role: ${requiredRole}, User roles: ${context.user.roles.join(', ')}`
      );
    }
    
    return true;
  };
}

// 所有権条件
function owns(resourceIdField: string) {
  return async (input: any, context: AuthContext): Promise<boolean> => {
    const resourceId = input[resourceIdField];
    if (!resourceId) {
      throw new ContractError('MISSING_RESOURCE_ID', `Field ${resourceIdField} is required`);
    }
    
    // 管理者は全リソースにアクセス可能
    if (context.user.roles.includes('admin')) {
      return true;
    }
    
    // リソース所有権チェック
    const resource = await getResourceById(resourceId);
    if (!resource || resource.userId !== context.user.id) {
      throw new ContractError('OWNERSHIP_DENIED', 
        `User ${context.user.id} does not own resource ${resourceId}`
      );
    }
    
    return true;
  };
}

// 入力検証条件
function validates(schema: z.ZodSchema) {
  return (input: unknown): any => {
    try {
      return schema.parse(input);
    } catch (error) {
      if (error instanceof z.ZodError) {
        const messages = error.issues.map(issue => 
          `${issue.path.join('.')}: ${issue.message}`
        ).join(', ');
        throw new ContractError('VALIDATION_FAILED', 
          `Input validation failed: ${messages}`
        );
      }
      throw error;
    }
  };
}

// レート制限条件
function rateLimit(operation: string, maxPerMinute: number) {
  return async (input: any, context: AuthContext): Promise<boolean> => {
    const key = `rateLimit:${context.user.id}:${operation}`;
    const current = await getRateLimitCount(key);
    
    if (current >= maxPerMinute) {
      throw new ContractError('RATE_LIMIT_EXCEEDED', 
        `Rate limit exceeded for ${operation}: ${current}/${maxPerMinute} per minute`
      );
    }
    
    await incrementRateLimitCount(key);
    return true;
  };
}

// 出力検証条件
function returns(schema: z.ZodSchema) {
  return (output: any, input: any, context: any): boolean => {
    try {
      schema.parse(output);
      return true;
    } catch (error) {
      throw new ContractError('OUTPUT_VALIDATION_FAILED', 
        `Output does not match expected schema: ${error.message}`
      );
    }
  };
}

// 監査ログ条件
function auditLog(action: string) {
  return async (output: any, input: any, context: AuthContext): Promise<boolean> => {
    await logAuditEvent({
      action,
      userId: context.user.id,
      resourceId: input.id || input.userId,
      timestamp: new Date(),
      input: sanitizeForAudit(input),
      output: sanitizeForAudit(output),
      success: true
    });
    return true;
  };
}

// ビジネスルール条件
function businessRule(description: string, rule: (input: any, context: AuthContext) => boolean | Promise<boolean>) {
  return async (input: any, context: AuthContext): Promise<boolean> => {
    const passed = await rule(input, context);
    if (!passed) {
      throw new ContractError('BUSINESS_RULE_VIOLATION', description);
    }
    return true;
  };
}

4. 実装例:ユーザー管理システム

スキーマ定義

import { z } from 'zod';

// 入力スキーマ
const userUpdateSchema = z.object({
  userId: z.string().uuid('Invalid user ID format'),
  email: z.string().email('Invalid email format').optional(),
  name: z.string().min(1, 'Name is required').max(100, 'Name must be less than 100 characters').optional(),
  role: z.enum(['user', 'admin', 'moderator']).optional()
});

const userCreateSchema = z.object({
  email: z.string().email('Invalid email format'),
  name: z.string().min(1, 'Name is required').max(100, 'Name must be less than 100 characters'),
  role: z.enum(['user', 'admin', 'moderator']).default('user')
});

// 出力スキーマ
const userOutputSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(),
  role: z.string(),
  createdAt: z.date(),
  updatedAt: z.date()
});

// コンテキスト型
interface AuthContext {
  user: {
    id: string;
    email: string;
    roles: string[];
  };
  session: {
    id: string;
    expiresAt: Date;
  };
}

Presentation Layer

// app/(routes)/profile/page.tsx
import { redirect } from 'next/navigation';
import { updateProfile } from '@/lib/actions/users';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth/config';

export default async function ProfilePage() {
  const session = await getServerSession(authOptions);
  
  // Presentationレイヤーでの基本認証チェック
  if (!session?.user) {
    redirect('/login');
  }
  
  if (new Date(session.expires) < new Date()) {
    redirect('/login?error=session_expired');
  }

  return (
    <div>
      <h1>Profile Settings</h1>
      <form action={updateProfile}>
        <input name="email" type="email" defaultValue={session.user.email} required />
        <input name="name" type="text" defaultValue={session.user.name} required />
        <input name="userId" type="hidden" value={session.user.id} />
        <button type="submit">Update Profile</button>
      </form>
    </div>
  );
}

Action Layer

// lib/actions/users.ts
"use server";
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth/config';
import { userService } from '@/lib/services/users';
import { redirect } from 'next/navigation';

class UserActions {
  @contract({
    requires: [
      auth('user'),
      validates(userUpdateSchema),
      rateLimit('updateProfile', 5)
    ],
    ensures: [
      returns(userOutputSchema),
      auditLog('profile_update')
    ],
    layer: 'action'
  })
  async updateProfile(
    input: z.infer<typeof userUpdateSchema>,
    context: AuthContext
  ): Promise<z.infer<typeof userOutputSchema>> {
    return userService.updateUser(input, context);
  }

  @contract({
    requires: [
      auth('admin'),
      validates(userCreateSchema),
      rateLimit('createUser', 10)
    ],
    ensures: [
      returns(userOutputSchema),
      auditLog('user_creation')
    ],
    layer: 'action'
  })
  async createUser(
    input: z.infer<typeof userCreateSchema>,
    context: AuthContext
  ): Promise<z.infer<typeof userOutputSchema>> {
    return userService.createUser(input, context);
  }

  @contract({
    requires: [
      auth('user'),
      owns('userId'),
      rateLimit('deleteProfile', 1)
    ],
    ensures: [
      auditLog('profile_deletion')
    ],
    layer: 'action'
  })
  async deleteProfile(
    input: { userId: string },
    context: AuthContext
  ): Promise<{ success: boolean }> {
    await userService.deleteUser(input.userId, context);
    return { success: true };
  }
}

const userActions = new UserActions();

// Server Action エントリーポイント
export const updateProfile = async (formData: FormData) => {
  try {
    const session = await getServerSession(authOptions);
    const context = {
      user: session?.user,
      session: session
    } as AuthContext;

    const input = {
      userId: formData.get('userId') as string,
      email: formData.get('email') as string,
      name: formData.get('name') as string
    };

    const result = await userActions.updateProfile(input, context);
    
    return { success: true, data: result };
    
  } catch (error) {
    if (error instanceof ContractViolationError) {
      const response = error.getAppropriateResponse();
      
      if (response.redirect) {
        redirect(response.redirect);
      }
      
      return response;
    }
    
    console.error('Unexpected error in updateProfile:', error);
    return { success: false, error: 'An unexpected error occurred.' };
  }
};

export const createUser = async (formData: FormData) => {
  try {
    const session = await getServerSession(authOptions);
    const context = {
      user: session?.user,
      session: session
    } as AuthContext;

    const input = {
      email: formData.get('email') as string,
      name: formData.get('name') as string,
      role: formData.get('role') as 'user' | 'admin' | 'moderator'
    };

    const result = await userActions.createUser(input, context);
    return { success: true, data: result };
    
  } catch (error) {
    if (error instanceof ContractViolationError) {
      return error.getAppropriateResponse();
    }
    
    return { success: false, error: 'An unexpected error occurred.' };
  }
};

Business Layer

// lib/services/users.ts
import { userRepository } from '@/lib/data/users';

class UserService {
  @contract({
    requires: [
      owns('userId'),
      businessRule('Cannot change own role', (input, context) => {
        return !input.role || input.userId !== context.user.id || input.role === context.user.roles[0];
      }),
      businessRule('Email change frequency limit', async (input, context) => {
        if (!input.email) return true;
        const user = await userRepository.findById(input.userId);
        const lastChange = user?.emailChangedAt;
        if (!lastChange) return true;
        
        const hoursSince = (Date.now() - lastChange.getTime()) / (1000 * 60 * 60);
        return hoursSince >= 24;
      })
    ],
    invariants: [
      // 出力されるユーザーIDは入力と同じでなければならない
      (input, output) => output.id === input.userId,
      // 更新時刻は現在時刻以前でなければならない
      (input, output) => output.updatedAt <= new Date()
    ],
    layer: 'business'
  })
  async updateUser(
    input: UserUpdateInput,
    context: AuthContext
  ): Promise<User> {
    const existingUser = await userRepository.findById(input.userId);
    if (!existingUser) {
      throw new Error('User not found');
    }
    
    // ビジネスロジック実行
    const updatedUser = {
      ...existingUser,
      ...(input.email && { email: input.email }),
      ...(input.name && { name: input.name }),
      ...(input.role && { role: input.role }),
      updatedAt: new Date(),
      ...(input.email && input.email !== existingUser.email && { 
        emailChangedAt: new Date() 
      })
    };
    
    // メール重複チェック
    if (input.email && input.email !== existingUser.email) {
      const duplicate = await userRepository.findByEmail(input.email);
      if (duplicate && duplicate.id !== input.userId) {
        throw new Error('Email already in use');
      }
    }
    
    return userRepository.save(updatedUser, context);
  }

  @contract({
    requires: [
      businessRule('Email must be unique', async (input) => {
        const existing = await userRepository.findByEmail(input.email);
        return !existing;
      })
    ],
    invariants: [
      (input, output) => output.email === input.email,
      (input, output) => output.name === input.name,
      (input, output) => output.role === input.role
    ],
    layer: 'business'
  })
  async createUser(
    input: UserCreateInput,
    context: AuthContext
  ): Promise<User> {
    const newUser = {
      id: crypto.randomUUID(),
      email: input.email,
      name: input.name,
      role: input.role,
      userId: crypto.randomUUID(), // for ownership tracking
      createdAt: new Date(),
      updatedAt: new Date()
    };
    
    return userRepository.create(newUser, context);
  }

  @contract({
    requires: [
      owns('userId'),
      businessRule('Cannot delete admin users', async (userId, context) => {
        const user = await userRepository.findById(userId);
        return user?.role !== 'admin' || context.user.roles.includes('super_admin');
      })
    ],
    layer: 'business'
  })
  async deleteUser(userId: string, context: AuthContext): Promise<void> {
    await userRepository.delete(userId, context);
  }
}

export const userService = new UserService();

// 型定義
interface UserUpdateInput {
  userId: string;
  email?: string;
  name?: string;
  role?: string;
}

interface UserCreateInput {
  email: string;
  name: string;
  role: string;
}

interface User {
  id: string;
  email: string;
  name: string;
  role: string;
  userId: string;
  createdAt: Date;
  updatedAt: Date;
  emailChangedAt?: Date;
}

Data Layer

// lib/data/users.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

class UserRepository {
  @contract({
    requires: [
      // データアクセス権限の最終チェック
      (input, context) => {
        return context.user.roles.includes('admin') || 
               input.id === context.user.id;
      }
    ],
    ensures: [
      // データ整合性の最終確認
      (output, input) => output.id === input.id,
      auditLog('user_data_update')
    ],
    invariants: [
      // 保存されたデータの整合性
      (input, output) => output.updatedAt >= input.updatedAt
    ],
    layer: 'data'
  })
  async save(user: User, context: AuthContext): Promise<User> {
    const result = await prisma.user.update({
      where: { 
        id: user.id,
        // クエリレベルでの最終的な安全装置
        ...(context.user.roles.includes('admin') ? {} : { id: context.user.id })
      },
      data: {
        email: user.email,
        name: user.name,
        role: user.role,
        updatedAt: user.updatedAt,
        emailChangedAt: user.emailChangedAt
      }
    });
    
    return result;
  }

  @contract({
    ensures: [
      auditLog('user_data_creation')
    ],
    invariants: [
      (input, output) => output.email === input.email,
      (input, output) => output.name === input.name
    ],
    layer: 'data'
  })
  async create(user: User, context: AuthContext): Promise<User> {
    const result = await prisma.user.create({
      data: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
        userId: user.userId,
        createdAt: user.createdAt,
        updatedAt: user.updatedAt
      }
    });
    
    return result;
  }

  @contract({
    requires: [
      // 削除権限の最終チェック
      (input, context) => {
        return context.user.roles.includes('admin') || 
               input === context.user.id;
      }
    ],
    ensures: [
      auditLog('user_data_deletion')
    ],
    layer: 'data'
  })
  async delete(userId: string, context: AuthContext): Promise<void> {
    await prisma.user.delete({
      where: { 
        id: userId,
        // クエリレベルでの安全装置
        ...(context.user.roles.includes('admin') ? {} : { id: context.user.id })
      }
    });
  }

  // 契約なしのヘルパーメソッド(内部でのみ使用)
  async findById(userId: string): Promise<User | null> {
    return prisma.user.findUnique({
      where: { id: userId }
    });
  }

  async findByEmail(email: string): Promise<User | null> {
    return prisma.user.findUnique({
      where: { email }
    });
  }
}

export const userRepository = new UserRepository();

5. 高度な契約パターン

契約の合成と継承

// 共通契約の定義
const baseUserContract = {
  requires: [auth('user')],
  ensures: [auditLog('user_operation')]
};

const adminUserContract = {
  requires: [auth('admin'), rateLimit('admin_operation', 20)],
  ensures: [auditLog('admin_operation')]
};

// 契約の合成
function composeContracts(...contracts: Partial<ContractOptions>[]): ContractOptions {
  return {
    requires: contracts.flatMap(c => c.requires || []),
    ensures: contracts.flatMap(c => c.ensures || []),
    invariants: contracts.flatMap(c => c.invariants || [])
  };
}

// 使用例
@contract(composeContracts(
  baseUserContract,
  {
    requires: [validates(userUpdateSchema), owns('userId')],
    invariants: [(input, output) => output.id === input.userId]
  }
))
async updateUserProfile(input: UserUpdateInput, context: AuthContext) {
  // 実装
}

条件付き契約

// 条件付き契約の実装
function conditionalContract(
  condition: (input: any, context: any) => boolean,
  whenTrue: ContractOptions,
  whenFalse: ContractOptions = {}
) {
  return function(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args: any[]) {
      const [input, context] = args;
      const contractToApply = condition(input, context) ? whenTrue : whenFalse;
      
      // 選択された契約を適用
      const contractDecorator = contract(contractToApply);
      const tempDescriptor = { value: originalMethod };
      contractDecorator(target, propertyName, tempDescriptor);
      
      return tempDescriptor.value.apply(this, args);
    };
  };
}

// 使用例
@conditionalContract(
  (input, context) => input.role && input.role !== context.user.roles[0],
  // ロール変更時は追加チェック
  { requires: [auth('admin'), businessRule('Role change requires admin', () => true)] },
  // 通常の更新
  { requires: [auth('user'), owns('userId')] }
)
async updateUser(input: UserUpdateInput, context: AuthContext) {
  // 実装
}

バルクオペレーション契約

// バルクオペレーション用の契約
function bulkContract(itemContract: ContractOptions) {
  return {
    requires: [
      (input: any[], context: AuthContext) => {
        if (!Array.isArray(input)) {
          throw new ContractError('INVALID_BULK_INPUT', 'Input must be an array');
        }
        return true;
      }
    ],
    ensures: [
      async (output: any[], input: any[], context: AuthContext) => {
        // 各アイテムに個別の契約を適用
        for (let i = 0; i < input.length; i++) {
          for (const condition of itemContract.ensures || []) {
            await condition(output[i], input[i], context);
          }
        }
        return true;
      }
    ]
  };
}

@contract(bulkContract({
  requires: [owns('userId')],
  ensures: [auditLog('bulk_user_update')]
}))
async bulkUpdateUsers(users: UserUpdateInput[], context: AuthContext): Promise<User[]> {
  const results = [];
  for (const user of users) {
    results.push(await this.updateUser(user, context));
  }
  return results;
}

6. デバッグとモニタリング

開発環境での契約可視化

class ContractDebugger {
  private static contractHistory: Array<{
    contractName: string;
    layer: string;
    timestamp: Date;
    input: any;
    output?: any;
    status: 'success' | 'failure';
    error?: any;
  }> = [];

  static logContractExecution(
    contractName: string,
    layer: string,
    input: any,
    output?: any,
    error?: any
  ) {
    if (process.env.NODE_ENV === 'development') {
      this.contractHistory.push({
        contractName,
        layer,
        timestamp: new Date(),
        input: this.sanitizeForLog(input),
        output: this.sanitizeForLog(output),
        status: error ? 'failure' : 'success',
        error
      });
      
      // コンソールでリアルタイム表示
      const icon = error ? '❌' : '✅';
      console.log(`${icon} Contract [${layer}] ${contractName}`, {
        input: this.sanitizeForLog(input),
        ...(output && { output: this.sanitizeForLog(output) }),
        ...(error && { error: error.message })
      });
    }
  }

  static getContractReport(): string {
    const successCount = this.contractHistory.filter(h => h.status === 'success').length;
    const failureCount = this.contractHistory.filter(h => h.status === 'failure').length;
    
    const layerStats = this.contractHistory.reduce((acc, h) => {
      acc[h.layer] = acc[h.layer] || { success: 0, failure: 0 };
      acc[h.layer][h.status]++;
      return acc;
    }, {} as Record<string, { success: number; failure: number }>);

    return JSON.stringify({
      total: this.contractHistory.length,
      success: successCount,
      failure: failureCount,
      successRate: `${((successCount / this.contractHistory.length) * 100).toFixed(1)}%`,
      layerStats
    }, null, 2);
  }

  private static sanitizeForLog(data: any): any {
    if (typeof data !== 'object' || data === null) {
      return data;
    }
    
    const sanitized = { ...data };
    // 機密情報を除外
    delete sanitized.password;
    delete sanitized.token;
    delete sanitized.secret;
    
    return sanitized;
  }
}

// 開発環境でのグローバル契約モニター
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
  (window as any).__contractDebugger = ContractDebugger;
}

パフォーマンス測定

class ContractPerformanceMonitor {
  private static metrics = new Map<string, {
    executions: number;
    totalTime: number;
    failures: number;
    avgTime: number;
  }>();

  static measureContract<T>(
    contractName: string,
    fn: () => Promise<T> | T
  ): Promise<T> | T {
    const start = performance.now();
    
    const updateMetrics = (success: boolean) => {
      const duration = performance.now() - start;
      const current = this.metrics.get(contractName) || {
        executions: 0,
        totalTime: 0,
        failures: 0,
        avgTime: 0
      };
      
      current.executions++;
      current.totalTime += duration;
      if (!success) current.failures++;
      current.avgTime = current.totalTime / current.executions;
      
      this.metrics.set(contractName, current);
    };

    try {
      const result = fn();
      
      if (result instanceof Promise) {
        return result
          .then(res => {
            updateMetrics(true);
            return res;
          })
          .catch(err => {
            updateMetrics(false);
            throw err;
          });
      }
      
      updateMetrics(true);
      return result;
    } catch (error) {
      updateMetrics(false);
      throw error;
    }
  }

  static getPerformanceReport(): string {
    const report = Array.from(this.metrics.entries())
      .map(([name, stats]) => ({
        contract: name,
        executions: stats.executions,
        avgTimeMs: parseFloat(stats.avgTime.toFixed(2)),
        failureRate: `${((stats.failures / stats.executions) * 100).toFixed(1)}%`,
        totalTimeMs: parseFloat(stats.totalTime.toFixed(2))
      }))
      .sort((a, b) => b.avgTimeMs - a.avgTimeMs);
    
    return JSON.stringify(report, null, 2);
  }
}

7. テスト戦略

契約のユニットテスト

describe('Contract System', () => {
  describe('Authentication Contract', () => {
    it('should pass with valid user and role', async () => {
      const context = {
        user: { id: 'user-123', roles: ['user'] },
        session: { id: 'session-123', expiresAt: new Date(Date.now() + 3600000) }
      };
      
      const authCondition = auth('user');
      await expect(authCondition({}, context)).resolves.toBe(true);
    });
    
    it('should fail with insufficient role', async () => {
      const context = {
        user: { id: 'user-123', roles: ['user'] },
        session: { id: 'session-123', expiresAt: new Date(Date.now() + 3600000) }
      };
      
      const authCondition = auth('admin');
      await expect(authCondition({}, context))
        .rejects
        .toThrow('Required role: admin');
    });
    
    it('should fail with expired session', async () => {
      const context = {
        user: { id: 'user-123', roles: ['user'] },
        session: { id: 'session-123', expiresAt: new Date(Date.now() - 1000) }
      };
      
      const authCondition = auth('user');
      await expect(authCondition({}, context))
        .rejects
        .toThrow('Session has expired');
    });
  });

  describe('Ownership Contract', () => {
    beforeEach(() => {
      // リソース取得のモック
      jest.spyOn(global, 'getResourceById').mockImplementation(async (id) => {
        const resources = {
          'resource-123': { id: 'resource-123', userId: 'user-123' },
          'resource-456': { id: 'resource-456', userId: 'user-456' }
        };
        return resources[id] || null;
      });
    });

    it('should pass for resource owner', async () => {
      const context = {
        user: { id: 'user-123', roles: ['user'] }
      };
      
      const ownershipCondition = owns('resourceId');
      await expect(ownershipCondition({ resourceId: 'resource-123' }, context))
        .resolves.toBe(true);
    });
    
    it('should pass for admin', async () => {
      const context = {
        user: { id: 'user-456', roles: ['admin'] }
      };
      
      const ownershipCondition = owns('resourceId');
      await expect(ownershipCondition({ resourceId: 'resource-123' }, context))
        .resolves.toBe(true);
    });
    
    it('should fail for non-owner', async () => {
      const context = {
        user: { id: 'user-456', roles: ['user'] }
      };
      
      const ownershipCondition = owns('resourceId');
      await expect(ownershipCondition({ resourceId: 'resource-123' }, context))
        .rejects
        .toThrow('does not own resource');
    });
  });
});

インテグレーションテスト

describe('User Management Integration', () => {
  let mockContext: AuthContext;
  let userActions: UserActions;

  beforeEach(() => {
    mockContext = {
      user: { id: 'user-123', email: 'user@example.com', roles: ['user'] },
      session: { id: 'session-123', expiresAt: new Date(Date.now() + 3600000) }
    };
    
    userActions = new UserActions();
  });

  describe('updateProfile', () => {
    it('should successfully update own profile', async () => {
      const input = {
        userId: 'user-123',
        email: 'newemail@example.com',
        name: 'New Name'
      };
      
      // モックのセットアップ
      jest.spyOn(userService, 'updateUser').mockResolvedValue({
        id: 'user-123',
        email: 'newemail@example.com',
        name: 'New Name',
        role: 'user',
        createdAt: new Date(),
        updatedAt: new Date()
      });
      
      const result = await userActions.updateProfile(input, mockContext);
      
      expect(result.email).toBe('newemail@example.com');
      expect(result.name).toBe('New Name');
    });
    
    it('should fail when trying to update other user profile', async () => {
      const input = {
        userId: 'user-456', // Different user
        email: 'newemail@example.com',
        name: 'New Name'
      };
      
      await expect(userActions.updateProfile(input, mockContext))
        .rejects
        .toThrow(ContractViolationError);
    });
    
    it('should fail with invalid email format', async () => {
      const input = {
        userId: 'user-123',
        email: 'invalid-email',
        name: 'New Name'
      };
      
      await expect(userActions.updateProfile(input, mockContext))
        .rejects
        .toThrow('Input validation failed');
    });
  });

  describe('createUser', () => {
    beforeEach(() => {
      // 管理者コンテキスト
      mockContext.user.roles = ['admin'];
    });

    it('should successfully create user as admin', async () => {
      const input = {
        email: 'newuser@example.com',
        name: 'New User',
        role: 'user' as const
      };
      
      jest.spyOn(userService, 'createUser').mockResolvedValue({
        id: 'new-user-id',
        email: 'newuser@example.com',
        name: 'New User',
        role: 'user',
        createdAt: new Date(),
        updatedAt: new Date()
      });
      
      const result = await userActions.createUser(input, mockContext);
      
      expect(result.email).toBe('newuser@example.com');
      expect(result.role).toBe('user');
    });
    
    it('should fail when non-admin tries to create user', async () => {
      mockContext.user.roles = ['user'];
      
      const input = {
        email: 'newuser@example.com',
        name: 'New User',
        role: 'user' as const
      };
      
      await expect(userActions.createUser(input, mockContext))
        .rejects
        .toThrow('Required role: admin');
    });
  });
});

契約モックシステム

class ContractMocker {
  private mocks = new Map<string, any>();
  
  mockCondition(conditionName: string, result: any) {
    this.mocks.set(conditionName, result);
  }
  
  mockAuth(result: boolean | Error) {
    this.mockCondition('auth', result);
  }
  
  mockOwnership(result: boolean | Error) {
    this.mockCondition('owns', result);
  }
  
  mockValidation(result: any | Error) {
    this.mockCondition('validates', result);
  }
  
  applyMocks() {
    // 実際の契約条件をモックで置き換え
    const originalAuth = global.auth;
    global.auth = () => async () => {
      const mock = this.mocks.get('auth');
      if (mock instanceof Error) throw mock;
      return mock ?? true;
    };
    
    // テスト後にリストア
    return () => {
      global.auth = originalAuth;
    };
  }
}

// テストでの使用例
describe('Contract Mocking', () => {
  let contractMocker: ContractMocker;
  let restoreMocks: () => void;

  beforeEach(() => {
    contractMocker = new ContractMocker();
    restoreMocks = contractMocker.applyMocks();
  });

  afterEach(() => {
    restoreMocks();
  });

  it('should handle auth failure gracefully', async () => {
    contractMocker.mockAuth(new ContractError('AUTH_FAILED', 'Mock auth failure'));
    
    await expect(userActions.updateProfile(validInput, mockContext))
      .rejects
      .toThrow('Mock auth failure');
  });
});

8. AI-Friendly設計パターン

契約テンプレートシステム

// AIが使いやすい事前定義契約
export const ContractTemplates = {
  // 基本的なCRUD操作
  userCRUD: (requiredRole: string = 'user') => ({
    requires: [
      auth(requiredRole),
      validates(userUpdateSchema),
      owns('userId'),
      rateLimit('userCRUD', 10)
    ],
    ensures: [
      returns(userOutputSchema),
      auditLog('user_crud')
    ]
  }),

  // 管理者専用操作
  adminOnly: (operation: string) => ({
    requires: [
      auth('admin'),
      rateLimit(`admin_${operation}`, 20)
    ],
    ensures: [
      auditLog(`admin_${operation}`)
    ]
  }),

  // 公開API(認証不要)
  publicAPI: (operation: string) => ({
    requires: [
      validates(z.any()), // 基本的な入力検証のみ
      rateLimit(`public_${operation}`, 100)
    ],
    ensures: [
      auditLog(`public_${operation}`)
    ]
  }),

  // バッチ処理用
  batchOperation: (itemContract: ContractOptions) => ({
    requires: [
      auth('admin'),
      (input: any[]) => {
        if (!Array.isArray(input)) {
          throw new ContractError('INVALID_BATCH_INPUT', 'Input must be an array');
        }
        if (input.length > 1000) {
          throw new ContractError('BATCH_TOO_LARGE', 'Batch size must be ≤ 1000 items');
        }
        return true;
      }
    ],
    ensures: [
      auditLog('batch_operation')
    ]
  })
};

// AIが生成するコード例
class AIGeneratedUserController {
  // AI: "ユーザープロフィール更新機能を作って"
  @contract(ContractTemplates.userCRUD())
  async updateUserProfile(input: UserUpdateInput, context: AuthContext) {
    return userService.updateUser(input, context);
  }

  // AI: "管理者によるユーザー削除機能を作って"  
  @contract(ContractTemplates.adminOnly('deleteUser'))
  async deleteUser(input: { userId: string }, context: AuthContext) {
    return userService.deleteUser(input.userId, context);
  }

  // AI: "公開ユーザー検索APIを作って"
  @contract(ContractTemplates.publicAPI('searchUsers'))
  async searchUsers(input: { query: string }, context: AuthContext) {
    return userService.searchUsers(input.query);
  }
}

スマート契約生成

// AIが契約を自動推論するヘルパー
function smartContract(options: {
  operation: 'create' | 'read' | 'update' | 'delete';
  resource: string;
  visibility: 'public' | 'private' | 'admin';
  rateLimit?: number;
}) {
  const contracts: ContractOptions = {
    requires: [],
    ensures: [],
    invariants: []
  };

  // 可視性に基づく認証要件
  switch (options.visibility) {
    case 'public':
      // 認証不要
      break;
    case 'private':
      contracts.requires!.push(auth('user'));
      if (options.operation !== 'create') {
        contracts.requires!.push(owns(`${options.resource}Id`));
      }
      break;
    case 'admin':
      contracts.requires!.push(auth('admin'));
      break;
  }

  // 操作に基づく検証要件
  if (['create', 'update'].includes(options.operation)) {
    // 作成・更新時は入力検証必須
    contracts.requires!.push(
      validates(getSchemaForResource(options.resource, options.operation))
    );
  }

  // レート制限
  if (options.rateLimit) {
    contracts.requires!.push(
      rateLimit(`${options.operation}_${options.resource}`, options.rateLimit)
    );
  }

  // 監査ログ
  contracts.ensures!.push(
    auditLog(`${options.operation}_${options.resource}`)
  );

  // 出力検証
  if (['create', 'read', 'update'].includes(options.operation)) {
    contracts.ensures!.push(
      returns(getSchemaForResource(options.resource, 'output'))
    );
  }

  return contracts;
}

// AIが使用する例
class SmartUserController {
  // AI: "ユーザー作成機能"と指定するだけで適切な契約が自動適用
  @contract(smartContract({
    operation: 'create',
    resource: 'user',
    visibility: 'admin',
    rateLimit: 5
  }))
  async createUser(input: UserCreateInput, context: AuthContext) {
    return userService.createUser(input, context);
  }

  // AI: "ユーザー更新機能"
  @contract(smartContract({
    operation: 'update',
    resource: 'user', 
    visibility: 'private',
    rateLimit: 10
  }))
  async updateUser(input: UserUpdateInput, context: AuthContext) {
    return userService.updateUser(input, context);
  }
}

function getSchemaForResource(resource: string, operation: string): z.ZodSchema {
  const schemas = {
    user: {
      create: userCreateSchema,
      update: userUpdateSchema,
      output: userOutputSchema
    }
    // 他のリソースのスキーマも定義
  };
  
  return schemas[resource]?.[operation] || z.any();
}

9. 本番環境での運用

エラーハンドリングとフォールバック

// 本番環境用のエラーハンドリング
class ProductionErrorHandler {
  static handleContractViolation(error: ContractViolationError): any {
    // セキュリティログ記録
    this.logSecurityEvent(error);
    
    // レイヤーに応じた適切な応答
    switch (error.layer) {
      case 'presentation':
        return { redirect: '/login' };
      case 'action':
        return { 
          success: false, 
          error: this.getSafeErrorMessage(error.originalError)
        };
      case 'business':
        return { 
          success: false, 
          error: 'Permission denied' 
        };
      case 'data':
        return { 
          success: false, 
          error: 'Operation failed' 
        };
      default:
        return { 
          success: false, 
          error: 'An error occurred' 
        };
    }
  }

  private static getSafeErrorMessage(error: any): string {
    // 本番環境では詳細なエラー情報を隠す
    if (process.env.NODE_ENV === 'production') {
      const safeErrors = [
        'VALIDATION_FAILED',
        'RATE_LIMIT_EXCEEDED',
        'AUTHENTICATION_REQUIRED'
      ];
      
      if (error instanceof ContractError && safeErrors.includes(error.type)) {
        return error.message;
      }
      
      return 'Invalid request';
    }
    
    return error?.message || 'An error occurred';
  }

  private static logSecurityEvent(error: ContractViolationError) {
    // セキュリティ監視システムへの通知
    console.error('Security Event:', {
      type: 'contract_violation',
      layer: error.layer,
      contract: error.contractName,
      error: error.originalError?.type,
      timestamp: new Date(),
      // 本番環境では個人情報を除外
      ...(process.env.NODE_ENV !== 'production' && {
        details: error.originalError
      })
    });
  }
}

// グローバルエラーハンドラーへの統合
process.on('unhandledRejection', (reason, promise) => {
  if (reason instanceof ContractViolationError) {
    ProductionErrorHandler.handleContractViolation(reason);
  }
});

パフォーマンス最適化

// 契約の最適化とキャッシング
class OptimizedContractSystem {
  private static contractCache = new Map<string, Function>();
  private static conditionCache = new Map<string, any>();

  // 契約のコンパイルとキャッシング
  static compileContract(contractName: string, options: ContractOptions): Function {
    if (this.contractCache.has(contractName)) {
      return this.contractCache.get(contractName)!;
    }

    const compiledContract = this.optimizeContract(options);
    this.contractCache.set(contractName, compiledContract);
    
    return compiledContract;
  }

  private static optimizeContract(options: ContractOptions): Function {
    // 静的チェックと動的チェックを分離
    const staticChecks = this.extractStaticChecks(options.requires || []);
    const dynamicChecks = this.extractDynamicChecks(options.requires || []);

    return async (input: any, context: any) => {
      // 静的チェックは事前実行(コンパイル時)
      for (const check of staticChecks) {
        check(input, context);
      }

      // 動的チェックのみ実行時に実行
      for (const check of dynamicChecks) {
        await check(input, context);
      }
    };
  }

  // バリデーションスキーマのキャッシング
  static getCachedValidation(schemaKey: string, schema: z.ZodSchema) {
    if (!this.conditionCache.has(schemaKey)) {
      const compiledSchema = schema.parse.bind(schema);
      this.conditionCache.set(schemaKey, compiledSchema);
    }
    
    return this.conditionCache.get(schemaKey);
  }

  private static extractStaticChecks(conditions: any[]): Function[] {
    return conditions.filter(c => 
      // 静的に評価可能な条件
      typeof c === 'function' && c.toString().includes('input.') && !c.toString().includes('await')
    );
  }

  private static extractDynamicChecks(conditions: any[]): Function[] {
    return conditions.filter(c => 
      // 動的評価が必要な条件
      typeof c === 'function' && (c.toString().includes('await') || c.toString().includes('async'))
    );
  }
}

10. まとめ

このデコレーター契約プログラミングアプローチは、AIコーディング時代の課題を以下のように解決します:

解決される問題

  1. 認可の混乱: @contract({ requires: [auth('user'), owns('userId')] })で明確に宣言
  2. 品質のバラつき: 契約違反は自動的にエラーとして検出
  3. デバッグ困難: 契約名とレイヤーを含む詳細なエラー情報
  4. 規約違反: レイヤーごとの契約により責務分離を強制

実現される価値

  • 宣言的セキュリティ: 契約がコードの一部として可視化
  • AIフレンドリー: テンプレートとスマート生成による学習支援
  • 段階的導入: 必要な機能から契約を適用可能
  • 本番運用対応: パフォーマンス最適化とエラーハンドリング

適用効果

// Before: AIが生成する危険なコード
export async function updateUser(data: any) {
  return db.user.update({ where: { id: data.id }, data });
}

// After: 契約により自動的に安全なコード
@contract({
  requires: [auth('user'), validates(userUpdateSchema), owns('userId')],
  ensures: [returns(userOutputSchema), auditLog('user_update')]
})
export async function updateUser(data: UserUpdateInput, context: AuthContext) {
  return db.user.update({ where: { id: data.userId }, data });
}

AIが生成するコードに宣言的な契約を自動適用することで、開発速度と品質の両立を実現する実用的なアプローチです。デコレーターによる簡潔な記述により、複雑な契約システムを隠蔽しつつ、強力な型安全性とランタイム検証を提供します。

Discussion