UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

270 lines 9.22 kB
/** * CapabilityIndex - Capability-based extension discovery system * * @implements @.aiwg/requirements/use-cases/UC-004-extension-system.md * @architecture @.aiwg/architecture/software-architecture-doc.md * @tests @test/unit/extensions/capability-index.test.ts */ /** * CapabilityIndex enables finding extensions by what they can do. * * Maintains bidirectional mappings: * - capability → extension IDs (for queries) * - extension ID → capabilities (for updates) * * @example * ```typescript * const index = new CapabilityIndex(); * index.index(myExtension); * * // Find extensions that can format markdown * const formatters = index.getByCapability('format:markdown'); * * // Find extensions that can format OR lint * const tools = index.query({ any: ['format:markdown', 'lint:markdown'] }); * * // Find formatters that are NOT linters * const pureFormatters = index.query({ * all: ['format:markdown'], * not: ['lint:markdown'] * }); * ``` */ export class CapabilityIndex { /** Map of capability → extension IDs */ byCapability = new Map(); /** Map of extension ID → capabilities */ byExtension = new Map(); /** Map of extension ID → extension type */ extensionTypes = new Map(); /** * Index an extension's capabilities for fast lookup. * * @param extension - Extension to index * * @example * ```typescript * index.index({ * id: 'markdown-formatter', * type: 'tool', * capabilities: ['format:markdown', 'format:commonmark'] * }); * ``` */ index(extension) { const extensionId = extension.id; const capabilities = extension.capabilities || []; // Store extension type this.extensionTypes.set(extensionId, extension.type); // Remove old mappings if re-indexing this.remove(extensionId); // Store extension → capabilities mapping this.byExtension.set(extensionId, new Set(capabilities)); // Add to capability → extensions mappings for (const capability of capabilities) { if (!this.byCapability.has(capability)) { this.byCapability.set(capability, new Set()); } this.byCapability.get(capability).add(extensionId); } } /** * Remove an extension from the index. * * @param extensionId - ID of extension to remove * * @example * ```typescript * index.remove('markdown-formatter'); * ``` */ remove(extensionId) { // Get capabilities for this extension const capabilities = this.byExtension.get(extensionId); if (!capabilities) { return; // Not indexed } // Remove from capability → extensions mappings const capabilityArray = Array.from(capabilities); for (const capability of capabilityArray) { const extensions = this.byCapability.get(capability); if (extensions) { extensions.delete(extensionId); // Clean up empty sets if (extensions.size === 0) { this.byCapability.delete(capability); } } } // Remove extension → capabilities mapping this.byExtension.delete(extensionId); // Remove extension type this.extensionTypes.delete(extensionId); } /** * Query extensions by capabilities. * * Query logic: * 1. Start with all extensions if no `all` or `any` specified * 2. Filter to extensions with ALL capabilities in `all` array * 3. Filter to extensions with ANY capability in `any` array * 4. Exclude extensions with capabilities in `not` array * 5. Optionally filter by extension type * * @param query - Capability query * @returns Array of matching extension IDs * * @example * ```typescript * // Extensions that can do BOTH formatting and validation * index.query({ all: ['format:markdown', 'validate:markdown'] }); * * // Extensions that can do EITHER formatting or linting * index.query({ any: ['format:markdown', 'lint:markdown'] }); * * // Formatters that are NOT linters * index.query({ * all: ['format:markdown'], * not: ['lint:markdown'] * }); * * // Only tool-type extensions with formatting capability * index.query({ * all: ['format:markdown'], * type: 'tool' * }); * ``` */ query(query) { let candidates; // Phase 1: Build initial candidate set if (query.all && query.all.length > 0) { // Start with extensions that have the first required capability const firstCapability = this.byCapability.get(query.all[0]); candidates = firstCapability ? new Set(firstCapability) : new Set(); // Intersect with extensions that have ALL other required capabilities for (let i = 1; i < query.all.length; i++) { const withCapability = this.byCapability.get(query.all[i]); if (!withCapability || withCapability.size === 0) { // If any required capability has no extensions, result is empty return []; } // Keep only extensions that have this capability too const candidateArray = Array.from(candidates); candidates = new Set(candidateArray.filter(id => withCapability.has(id))); if (candidates.size === 0) { // Early exit if intersection is empty return []; } } } else if (query.any && query.any.length > 0) { // Union of extensions with ANY of the specified capabilities candidates = new Set(); for (const capability of query.any) { const withCapability = this.byCapability.get(capability); if (withCapability) { const capabilityArray = Array.from(withCapability); for (const id of capabilityArray) { candidates.add(id); } } } } else { // No constraints - start with all extensions candidates = new Set(this.byExtension.keys()); } // Phase 2: Apply exclusions if (query.not && query.not.length > 0) { for (const capability of query.not) { const toExclude = this.byCapability.get(capability); if (toExclude) { const excludeArray = Array.from(toExclude); for (const id of excludeArray) { candidates.delete(id); } } } } // Phase 3: Filter by type if (query.type) { const candidateArray = Array.from(candidates); candidates = new Set(candidateArray.filter(id => this.extensionTypes.get(id) === query.type)); } return Array.from(candidates).sort(); } /** * Get all capabilities in the index. * * @returns Sorted array of capability strings * * @example * ```typescript * const capabilities = index.getAllCapabilities(); * // ['format:markdown', 'lint:markdown', 'validate:markdown'] * ``` */ getAllCapabilities() { return Array.from(this.byCapability.keys()).sort(); } /** * Get all extensions with a specific capability. * * @param capability - Capability to search for * @returns Sorted array of extension IDs * * @example * ```typescript * const formatters = index.getByCapability('format:markdown'); * // ['markdown-formatter', 'prettier-markdown'] * ``` */ getByCapability(capability) { const extensions = this.byCapability.get(capability); return extensions ? Array.from(extensions).sort() : []; } /** * Check if a capability exists in the index. * * @param capability - Capability to check * @returns True if at least one extension has this capability * * @example * ```typescript * if (index.hasCapability('format:markdown')) { * console.log('Markdown formatting available'); * } * ``` */ hasCapability(capability) { const extensions = this.byCapability.get(capability); return extensions !== undefined && extensions.size > 0; } /** * Get the number of unique capabilities in the index. * * @example * ```typescript * console.log(`Indexed ${index.capabilityCount} capabilities`); * ``` */ get capabilityCount() { return this.byCapability.size; } /** * Clear all indexed data. * * @example * ```typescript * index.clear(); * // All mappings reset * ``` */ clear() { this.byCapability.clear(); this.byExtension.clear(); this.extensionTypes.clear(); } } //# sourceMappingURL=capability-index.js.map