/** * BaseElementManager - Abstract base class for all element managers * * Provides common CRUD operations for all element types using the * Template Method Pattern: * - Defines the skeleton of operations in base class * - Lets subclasses override specific steps without changing structure * * Subclasses: SkillManager, TemplateManager, AgentManager, MemoryManager * * SECURITY: * 1. CRITICAL: Uses FileLockManager for atomic read/write operations * 2. HIGH: Path validation and sanitization to prevent traversal attacks * 3. MEDIUM: Security event logging for audit trail * 4. MEDIUM: Input validation and sanitization throughout */ import { IElementManager } from '../../types/elements/IElementManager.js'; import { IElement, ElementValidationResult } from '../../types/elements/IElement.js'; import { ElementType } from '../../portfolio/types.js'; import { PortfolioManager } from '../../portfolio/PortfolioManager.js'; import { FileLockManager } from '../../security/fileLockManager.js'; import { LRUCache } from '../../cache/LRUCache.js'; import { ElementEventDispatcher, ElementEventPayload } from '../../events/ElementEventDispatcher.js'; import { FileWatchService } from '../../services/FileWatchService.js'; import { FileOperationsService } from '../../services/FileOperationsService.js'; import { ValidationRegistry } from '../../services/validation/ValidationRegistry.js'; import { type ElementValidator } from '../../services/validation/ElementValidator.js'; import type { IStorageLayer } from '../../storage/IStorageLayer.js'; import type { ElementIndexEntry } from '../../storage/types.js'; import type { CacheMemoryBudget } from '../../cache/CacheMemoryBudget.js'; import type { BackupService } from '../../services/BackupService.js'; export interface BaseElementManagerOptions { elementDirOverride?: string; eventDispatcher?: ElementEventDispatcher; elementCacheTTL?: number; pathCacheTTL?: number; enableFileWatcher?: boolean; autoReloadOnExternalChange?: boolean; fileWatchService?: FileWatchService; memoryBudget?: CacheMemoryBudget; backupService?: BackupService; } /** * Record of an element that failed to load (Issue #708). * Stored so callers can distinguish "file not found" from "file invalid". */ export interface InvalidElementRecord { /** Relative file path within the element directory. */ filePath: string; /** Human-readable reason the element was rejected. */ reason: string; /** ISO timestamp of last failed load attempt. */ failedAt: string; } /** * Abstract base class implementing common element management operations * Subclasses must implement element-specific logic via abstract methods */ export declare abstract class BaseElementManager implements IElementManager { protected portfolioManager: PortfolioManager; protected fileLockManager: FileLockManager; protected fileOperations: FileOperationsService; protected fileWatchService?: FileWatchService; protected elementDir: string; /** * Specialized validator for this element type * Obtained from ValidationRegistry during construction */ protected validator: ElementValidator; protected elements: LRUCache; private filePathToId; private readonly elementGenerations; private cacheGenerationCounter; private watcherCleanup?; private readonly eventDispatcher; private readonly autoReloadOnExternalChange; private readonly elementType; private readonly memoryBudget?; protected readonly backupService?: BackupService; /** Map plural ElementType enum values to singular ContentValidator context. * Partial because not all element types have a content context (e.g., ensembles). */ private static readonly ELEMENT_TYPE_TO_CONTEXT; protected readonly storageLayer: IStorageLayer; /** * Issue #708: Elements that exist on disk but failed validation during load. * Keyed by relative file path for deduplication. */ private readonly invalidElements; /** * Tracks file paths whose load failure has already been logged at error level. * Repeated failures with the same reason are demoted to debug to avoid log flooding. * Cleared when the file changes on disk or loads successfully. */ private readonly suppressedLoadPaths; /** * Returns true if the most recent load() failure for this path was suppressed * because it was a repeat of an already-logged error. * Subclasses can use this to avoid duplicate security event logging. * * @param filePath - Relative file path within the element directory * @returns Whether the error for this path is currently suppressed */ protected isLoadErrorSuppressed(filePath: string): boolean; protected afterLoad?(element: T, filePath: string): Promise; protected beforeSave?(element: T, filePath: string): Promise; protected afterSave?(element: T, filePath: string): Promise; protected findByIdentifier?(identifier: string): Promise; protected canDelete?(element: T): Promise<{ allowed: boolean; reason?: string; }>; /** * Create a backup before overwriting an existing file. * Subclasses can override to no-op (e.g. MemoryManager has its own backup system). */ protected createBackupBeforeSave(absolutePath: string): Promise; /** * Create a backup before deleting a file (moves file to backup dir). * Returns true if the original file was moved (caller should skip deleteFile). * Subclasses can override to no-op (e.g. MemoryManager has its own backup system). */ protected createBackupBeforeDelete(absolutePath: string): Promise; /** * Provides access to the event dispatcher for subclasses that need to emit custom events. * * BaseElementManager handles standard lifecycle events (load, save, delete) automatically. * Subclasses should use this getter only when they need to emit additional domain-specific * events that are not part of the standard CRUD lifecycle. * * @example * // PersonaManager emits activation/deactivation events * this.dispatcher.emit('element:activate', this.createEventPayload({...})); * * @returns The ElementEventDispatcher instance used by this manager */ protected get dispatcher(): ElementEventDispatcher; private static readonly MAX_ELEMENT_CACHE_SIZE; private static readonly MAX_PATH_CACHE_SIZE; /** * Constructor - initializes common dependencies * @param elementType - The type of element this manager handles * @param portfolioManager - Portfolio manager for directory resolution * @param fileLockManager - File lock manager for atomic operations * @param options - Configuration options including fileWatchService * @param fileOperationsService - Service for file operations * @param validationRegistry - Registry for obtaining type-specific validators */ constructor(elementType: ElementType, portfolioManager: PortfolioManager, fileLockManager: FileLockManager, options: BaseElementManagerOptions | undefined, fileOperationsService: FileOperationsService, validationRegistry: ValidationRegistry); /** * Factory method for creating the storage layer. * Default returns ElementStorageLayer for .md elements. * Subclasses (e.g. MemoryManager) can override to return a different implementation. */ protected createStorageLayer(fileOperationsService: FileOperationsService): IStorageLayer; /** * Returns the singular human-readable label for this element type (e.g., "skill", "persona"). * Used in filename generation ({name}-{label}.md) and display strings. * Must return the singular form — not the plural ElementType value. */ protected abstract getElementLabel(): string; /** * Returns a capitalized version of the element label. */ protected getElementLabelCapitalized(): string; /** * Load an element from file * TEMPLATE METHOD: Defines the algorithm, subclasses customize steps * * SECURITY FIXES (inherited from original managers): * - this.fileLockManager.atomicReadFile() prevents race conditions * - Path validation prevents traversal attacks * - Security event logging for audit trail */ load(filePath: string): Promise; /** * Issue #695: Pre-fill missing metadata fields with sensible defaults * before parseMetadata() runs. This implements the "tolerant reader" pattern * — strict on output, lenient on input for older/sparse frontmatter. * * Mutates `data` in place. Logs a warning for each defaulted field so * operators know which files need updating. */ private migrateMetadataDefaults; /** * Issue #708: Returns elements that exist on disk but failed validation during load. * Callers can use this to report invalid elements instead of silently hiding them. */ getInvalidElements(): InvalidElementRecord[]; /** * Issue #708: Check if a specific file failed validation during load. * Used by get_element to distinguish "not found" from "invalid". */ getInvalidElement(name: string): InvalidElementRecord | undefined; /** * Save an element to file * TEMPLATE METHOD: Common save logic with hooks for customization * * SECURITY FIXES: * - this.fileLockManager.atomicWriteFile() for atomic operations * - Path validation to prevent traversal attacks * - Security event logging */ save(element: T, filePath: string): Promise; /** * Validate serialized element content before writing to disk. * Fix #908: Mirrors the read-path validation from SecureYamlParser.parse() * to ensure write → read symmetry. Content that fails this check would also * fail to load, so rejecting it on write prevents permanently broken elements. */ private validateSerializedContent; /** * List all available elements * SECURITY: Uses PortfolioManager.listElements() which filters test elements */ list(): Promise; /** * List lightweight metadata summaries without loading full elements. * Useful when only names/descriptions/tags are needed. */ listSummaries(): Promise; /** * Find an element by predicate */ find(predicate: (element: T) => boolean): Promise; /** * Find multiple elements by predicate */ findMany(predicate: (element: T) => boolean): Promise; /** * Find an element by name or ID without loading all elements * * Issue #24 (LOW PRIORITY): Performance optimization for activation flow * * This method provides an optimized lookup that tries cache first, then * attempts direct file access before falling back to full list() scan. * This is significantly faster than list() for large portfolios. * * PERFORMANCE IMPROVEMENTS: * 1. Cache lookup - O(1) if element was previously loaded * 2. Direct file access - O(1) for name-based lookups * 3. Full scan fallback - O(n) only if above methods fail * * @param identifier - Element name or ID to search for * @returns Element if found, undefined otherwise */ findByName(identifier: string): Promise; /** * Helper: Search cache for element by name or ID * @private */ private findInCache; /** * Helper: Try loading element directly by constructing expected filename * @private */ private tryDirectLoad; /** * Validate an element * Delegates to element's own validate method * * @returns Validation result with both 'valid' and 'isValid' properties. * 'isValid' is deprecated - use 'valid' for new code. */ validate(element: T): ElementValidationResult; /** * Delete an element * SECURITY: Path validation to prevent deletion outside directory * CACHE FIX: Uses filepath-based cache removal to prevent stale entries */ delete(filePath: string): Promise; /** * Check if an element exists */ exists(filePath: string): Promise; /** * Validate a file path */ validatePath(filePath: string): boolean; /** * Get the element type */ getElementType(): ElementType; /** * Resolves a file path to its absolute form for cache consistency * Ensures consistent path handling across relative/absolute paths */ protected resolveAbsolutePath(filePath: string): string; private normalizeAndValidatePath; private getCachedElementByAbsolutePath; /** * Protected cache lookup by absolute path. * Allows subclasses with custom load() overrides (e.g. MemoryManager) * to check the LRU cache before re-reading from disk. */ protected getCachedByAbsolutePath(absolutePath: string): T | undefined; private loadElementSnapshot; /** * Creates a standardized event payload for element lifecycle events. * * This helper is available to subclasses to ensure consistent event payload * structure when emitting custom events via the dispatcher getter. * * @param params - Event parameters including correlation ID, element, file path, and optional error * @returns Fully-formed ElementEventPayload ready for emission */ protected createEventPayload(params: { correlationId: string; filePath?: string; element?: T; error?: unknown; }): ElementEventPayload; private handleExternalChange; /** * Adds an element to both caches (bidirectional mapping) * @param element - Element to cache * @param filePath - File path (relative or absolute) */ protected cacheElement(element: T, filePath: string): void; /** * Force a fresh disk scan and evict any modified/removed entries from the * in-memory LRU cache. Call before findByName() when freshness is critical * (e.g. on ensemble activation) to pick up external file changes that * occurred since the last scan, even if the scan cooldown is still active. * * Unlike list(), this does not load all elements — it only evicts stale ones. * Fixes #1895 (ensemble activation serving stale cached element list). */ protected scanAndEvict(): Promise; /** * Removes an element from both caches by file path * This is the preferred method for deletion to avoid stale cache entries * @param filePath - File path (relative or absolute) */ protected uncacheByPath(filePath: string): void; /** * Clear all cached elements */ clearCache(): void; /** * Get cache statistics for debugging * @returns Object with cache size metrics */ protected getCacheStats(): { elementCount: number; pathMappings: number; }; /** * Expose internal LRU cache instances for metrics collection. */ getMetricsCaches(): Array<{ name: string; instance: LRUCache; }>; /** * Dispose of resources and cleanup * Subclasses should override to add their own cleanup logic */ dispose(): void; /** * Normalize a name to kebab-case for consistent filename formatting. * * This method provides unified filename normalization across all element managers, * ensuring consistent naming regardless of the input format (CamelCase, spaces, * underscores, mixed case, etc.). * * Transformations applied (in order): * 1. Insert hyphens between camelCase boundaries (MyName -> My-Name) * 2. Replace spaces and underscores with hyphens * 3. Convert to lowercase * 4. Strip invalid characters (keep only a-z, 0-9, -) * 5. Collapse multiple consecutive hyphens * 6. Trim leading/trailing hyphens * * @example * normalizeFilename("CRUDV-Agent-Delta") // "crudv-agent-delta" * normalizeFilename("Creative Writer") // "creative-writer" * normalizeFilename("CamelCaseName") // "camel-case-name" * normalizeFilename("my_skill_name") // "my-skill-name" * normalizeFilename("Special@Chars!") // "special-chars" * normalizeFilename("--leading-and-trailing--") // "leading-and-trailing" * * @param name - The element name to normalize * @returns Normalized kebab-case filename (without extension) */ protected normalizeFilename(name: string): string; /** * Get the standardized filename for an element. * * Normalizes the element name (handling CamelCase, spaces, underscores, etc.) * and appends the file extension. The directory structure (personas/, skills/, etc.) * already provides type context, so the type is NOT included in the filename. * * @param name - The element name to convert to a filename * @returns The standardized filename (e.g., "code-review.md") * * @example * getElementFilename("Code Review") // → "code-review.md" * getElementFilename("Debug Detective") // → "debug-detective.md" * getElementFilename("BugReport") // → "bug-report.md" (CamelCase split) * getElementFilename("fix-persona-helper") // → "fix-persona-helper.md" (no mangling) */ protected getElementFilename(name: string): string; /** * Parse and validate metadata from frontmatter * Subclasses implement element-specific validation logic * * @param data - Raw metadata from YAML frontmatter * @returns Validated and typed metadata */ protected abstract parseMetadata(data: any): Promise; /** * @deprecated Use ElementValidator via ValidationRegistry instead. * Will be removed in next major version. */ validateMetadata(metadata: any, _strict?: boolean): Promise; /** * Create an element instance from metadata and content * Subclasses implement element-specific construction * * @param metadata - Validated metadata * @param content - Element content (instructions, template, etc.) * @returns New element instance */ protected abstract createElement(metadata: T['metadata'], content: string): T; /** * Serialize an element to file content * Subclasses can customize serialization format * * @param element - Element to serialize * @returns File content (usually markdown with frontmatter) */ protected abstract serializeElement(element: T): Promise; /** * Get the file extension for this element type * Most elements use .md, but subclasses can override */ abstract getFileExtension(): string; /** * Import an element from external format * Subclasses implement format-specific import logic */ abstract importElement(data: string, format?: 'json' | 'yaml' | 'markdown'): Promise; /** * Export an element to external format * Subclasses implement format-specific export logic */ abstract exportElement(element: T, format?: 'json' | 'yaml' | 'markdown'): Promise; } //# sourceMappingURL=BaseElementManager.d.ts.map