schemaProvider.ts•12.4 kB
import * as vscode from 'vscode';
import * as path from 'path';
import { z } from 'zod';
type SchemaMap = { [key: string]: z.ZodSchema<unknown> };
/**
 * Provides access to memory bank document schemas.
 * Loads schemas dynamically from the compiled '@memory-bank/schemas' package.
 */
export class SchemaProvider {
  private schemas: SchemaMap = {};
  private workspaceRoot: string;
  private schemasPackageDistPath: string; // Path to the compiled schemas package dist
  constructor() {
    const workspaceFolders = vscode.workspace.workspaceFolders;
    if (!workspaceFolders || workspaceFolders.length === 0) {
      vscode.window.showErrorMessage('SchemaProvider requires an open workspace.');
      throw new Error('No workspace folder found.');
    }
    this.workspaceRoot = workspaceFolders[0].uri.fsPath;
    // Assuming schemas package is directly under 'packages/' and compiled to 'dist'
    // This might need adjustment based on actual monorepo linking (e.g., node_modules)
    this.schemasPackageDistPath = path.join(this.workspaceRoot, 'packages', 'schemas', 'dist');
    // 初期化情報をステータスバーに表示
    vscode.window.setStatusBarMessage(`Schema provider initialized: ${this.schemasPackageDistPath}`, 5000);
    // Load schemas asynchronously, don't block constructor
    this.loadSchemas().catch(() => {
        // エラーをキャッチするが使用しない
        vscode.window.showErrorMessage("Failed to initialize schemas");
        // Load placeholders as a fallback if initial dynamic load fails critically
        this.loadPlaceholderSchemas();
    });
  }
  /**
   * Dynamically loads Zod schemas from the compiled '@memory-bank/schemas' package.
   */
  private async loadSchemas(): Promise<void> {
    vscode.window.setStatusBarMessage('Loading schemas dynamically...', 3000);
    const loadedSchemas: SchemaMap = {}; // Load into a temporary map first
    const documentTypesDirUri = vscode.Uri.file(path.join(this.schemasPackageDistPath, 'document-types'));
    try {
      const entries = await vscode.workspace.fs.readDirectory(documentTypesDirUri);
      // デバッグ情報は削除
      const importPromises = entries
        .filter(([fileName, fileType]) => fileType === vscode.FileType.File && fileName.endsWith('.js'))
        .map(async ([fileName]) => {
          const modulePath = path.join(documentTypesDirUri.fsPath, fileName);
          const moduleUri = vscode.Uri.file(modulePath);
          
          try {
            // Use dynamic import. Ensure tsconfig allows this (module: NodeNext should work)
            const schemaModule = await import(moduleUri.fsPath);
            // Iterate over exports to find Zod schemas
            for (const exportName in schemaModule) {
              const exportedValue = schemaModule[exportName];
              // Check if the exported value is a Zod schema object
              if (exportedValue instanceof z.ZodSchema && exportedValue._def?.typeName === 'ZodObject') {
                 // Attempt to extract documentType literal from the schema definition
                 const shape = (exportedValue as z.ZodObject<z.ZodRawShape>).shape;
                 // Check if metadata and documentType exist and documentType is a ZodLiteral
                 if (shape?.metadata?._def?.typeName === 'ZodObject' && shape.metadata.shape?.documentType?._def?.typeName === 'ZodLiteral') {
                    const documentType = shape.metadata.shape.documentType._def.value;
                    if (typeof documentType === 'string') {
                        loadedSchemas[documentType] = exportedValue;
                        // デバッグ情報は削除
                    } else {
                        vscode.window.showWarningMessage(`Schema ${exportName} in ${fileName} has invalid documentType value`);
                    }
                 } else if (shape?.documentType?._def?.typeName === 'ZodLiteral') {
                    // Fallback for schemas where documentType might be directly in the root shape (like the discriminated union itself, though we load specifics here)
                    const documentType = shape.documentType._def.value;
                     if (typeof documentType === 'string') {
                        loadedSchemas[documentType] = exportedValue;
                        // デバッグ情報は削除
                    } else {
                        vscode.window.showWarningMessage(`Schema ${exportName} in ${fileName} has invalid documentType value (fallback)`);
                    }
                 }
                 else {
                    vscode.window.showWarningMessage(`Schema ${exportName} in ${fileName} has invalid structure`);
                 }
              }
            }
          } catch {
            // エラーをキャッチするが使用しない
            vscode.window.showErrorMessage(`Failed to load schema from ${fileName}`);
          }
        });
        await Promise.all(importPromises);
    } catch (readDirError) {
      // Handle case where directory might not exist (e.g., schemas not built)
      if (readDirError instanceof vscode.FileSystemError && readDirError.code === 'FileNotFound') {
          vscode.window.showWarningMessage(`Schemas directory not found. Please build the '@memory-bank/schemas' package.`);
      } else {
          vscode.window.showErrorMessage(`Failed to access schemas directory. Ensure '@memory-bank/schemas' is built.`);
      }
      // Fallback to placeholders if dynamic loading fails
      this.loadPlaceholderSchemas();
      return; // Exit early
    }
    // If dynamic loading succeeded, update the main schemas map
    if (Object.keys(loadedSchemas).length > 0) {
        this.schemas = loadedSchemas;
        vscode.window.setStatusBarMessage(`Schemas loaded: ${Object.keys(this.schemas).length}`, 3000);
    } else {
        vscode.window.showWarningMessage("No schemas were loaded dynamically. Using placeholders instead.");
        this.loadPlaceholderSchemas(); // Load placeholders if dynamic loading yielded nothing
    }
  }
  // Keep placeholder loading as a fallback or for testing
  private loadPlaceholderSchemas(): void {
      vscode.window.setStatusBarMessage("Loading placeholder schemas...", 2000);
      if (Object.keys(this.schemas).length > 0) {
          // デバッグコメントを削除
          return; // Avoid overwriting dynamically loaded schemas
      }
      try {
          // Define a basic metadata structure for placeholders
          const placeholderMetadata = z.object({
              id: z.string().uuid().optional(), // Make optional for flexibility in placeholders
              title: z.string().optional(),
              documentType: z.string(), // Keep this required to identify
              path: z.string().optional(),
              tags: z.array(z.string()).optional(),
              lastModified: z.string().datetime().optional(),
              createdAt: z.string().datetime().optional(),
              version: z.number().int().optional(),
          });
          // Placeholder for the base structure
          const basePlaceholder = z.object({
              schema: z.literal('memory_document_v2'),
              metadata: placeholderMetadata,
              content: z.unknown(),
          }).passthrough(); // Use passthrough for flexibility in placeholders
          // Placeholder for 'progress'
          const progressPlaceholder = basePlaceholder.extend({
              metadata: placeholderMetadata.extend({ documentType: z.literal('progress') }),
              content: z.object({
                  workingFeatures: z.array(z.object({ id: z.string(), description: z.string() })).optional(),
                  pendingImplementation: z.array(z.object({ id: z.string(), description: z.string(), priority: z.string() })).optional(),
                  status: z.string().optional(),
                  completionPercentage: z.number().min(0).max(100).optional(),
                  knownIssues: z.array(z.object({ id: z.string(), description: z.string() })).optional(),
              }).passthrough(), // Allow extra fields in content too
          });
          this.schemas['progress'] = progressPlaceholder;
          // デバッグコメントを削除
          // Add more placeholders if needed, e.g., for branch_context
           const branchContextPlaceholder = basePlaceholder.extend({
              metadata: placeholderMetadata.extend({ documentType: z.literal('branch_context') }),
              content: z.object({
                  branchName: z.string().optional(),
                  purpose: z.string().optional(),
                  createdAt: z.string().datetime().optional(),
                  userStories: z.array(z.object({ id: z.string(), description: z.string(), completed: z.boolean(), priority: z.number() })).optional(),
                  additionalNotes: z.string().optional(),
              }).passthrough(),
          });
          this.schemas['branch_context'] = branchContextPlaceholder;
          // デバッグコメントを削除
      } catch {
          // エラーをキャッチするが使用しない
          vscode.window.showErrorMessage("Error creating placeholder schemas");
      }
      vscode.window.setStatusBarMessage(`Placeholder schemas loaded: ${Object.keys(this.schemas).length}`, 3000);
  }
  /**
   * Retrieves a specific schema by its documentType identifier.
   * @param schemaId The documentType identifier for the schema.
   * @returns The Zod schema object, or undefined if not found.
   */
  public getSchema(schemaId: string): z.ZodSchema<unknown> | undefined {
    // Use documentType as the key
    return this.schemas[schemaId];
    // Consider if a fallback to a generic or base schema is needed if not found.
    // For example: return this.schemas[schemaId] ?? this.schemas['generic'];
  }
  /**
   * Validates a document object against a specific schema based on its documentType.
   * @param document The document object to validate (must have metadata.documentType).
   * @returns A ZodSafeParseReturnType indicating success or failure with errors.
   */
   public validateDocument(document: unknown): z.SafeParseReturnType<unknown, unknown> {
    // Basic check to see if it's an object with metadata and documentType
    if (typeof document !== 'object' || document === null || !('metadata' in document) || typeof document.metadata !== 'object' || document.metadata === null || !('documentType' in document.metadata) || typeof document.metadata.documentType !== 'string') {
        // Try to parse as base document first to get better error messages if structure is wrong
        const baseParse = z.object({ metadata: z.object({ documentType: z.string() }).passthrough() }).passthrough().safeParse(document);
        if (!baseParse.success) {
            return { success: false, error: baseParse.error };
        }
        // If base structure is okay but documentType is missing/invalid
        return { success: false, error: new z.ZodError([{ code: z.ZodIssueCode.custom, message: 'Invalid document structure: Missing or invalid metadata.documentType', path: ['metadata', 'documentType'] }]) };
    }
    const documentType = document.metadata.documentType;
    const schema = this.getSchema(documentType);
    if (!schema) {
      vscode.window.showWarningMessage(`Schema not found for documentType: '${documentType}'`);
      // Return a specific error structure indicating schema not found
      return { success: false, error: new z.ZodError([{ code: z.ZodIssueCode.custom, message: `Schema definition not found for documentType: '${documentType}'`, path: ['metadata', 'documentType'] }]) };
    }
    // デバッグコメントを削除
    
    // Use safeParse for detailed error reporting
    const result = schema.safeParse(document);
    if (!result.success) {
        vscode.window.showWarningMessage(`Document validation failed for '${documentType}'`);
    }
    return result;
  }
  // Add a method to get all loaded schema IDs (document types)
  public getAvailableSchemaIds(): string[] {
    return Object.keys(this.schemas);
  }
  // Force reload schemas (e.g., after building schemas package)
  public async reloadSchemas(): Promise<void> {
      vscode.window.setStatusBarMessage("Reloading schemas...", 2000);
      await this.loadSchemas();
  }
}