From 4b3ce2a4b9cb19bd65312c1e41a00b1ad20a63ef Mon Sep 17 00:00:00 2001 From: Nathan Allen Date: Sat, 7 Mar 2026 15:45:08 -0500 Subject: [PATCH 1/3] feat: implement inflector and naming engine --- .braids.json | 10 +- src/dictionaries/abbrs.json | 189 +++++++++++++ src/headers.ts | 4 +- src/http_client.ts | 6 +- src/parsers/inflector.ts | 160 +++++++++++ src/parsers/openapi_parser.ts | 105 ++------ src/parsers/tool_naming.ts | 312 +++++++++++++++++++++ tests/fixtures/test_spec.json | 1 - tests/fixtures/test_spec.yaml | 1 - tests/http_client.test.ts | 16 +- tests/inflector.test.ts | 328 ++++++++++++++++++++++ tests/openapi_parser.test.ts | 32 --- tests/schema_discovery.test.ts | 16 +- tests/tool_naming.test.ts | 478 +++++++++++++++++++++++++++++++++ 14 files changed, 1513 insertions(+), 145 deletions(-) create mode 100644 src/dictionaries/abbrs.json create mode 100644 src/parsers/inflector.ts create mode 100644 src/parsers/tool_naming.ts create mode 100644 tests/inflector.test.ts create mode 100644 tests/tool_naming.test.ts diff --git a/.braids.json b/.braids.json index 3cfb632..e420e79 100644 --- a/.braids.json +++ b/.braids.json @@ -1,11 +1,17 @@ { "config_version": 1, "mirrors": { - "src/schemas": { + "src/ocp_agent/schemas": { "url": "https://github.com/opencontextprotocol/ocp-spec.git", "branch": "main", "path": "schemas", - "revision": "b1ba770eb3e1bd245928088f1e7be0516d0fe820" + "revision": "86c82b4bdf319480d0a11ceac699e6c8a0f94a2b" + }, + "src/ocp_agent/dictionaries": { + "url": "https://github.com/opencontextprotocol/ocp-registry.git", + "branch": "main", + "path": "data/dictionaries", + "revision": "f322d11f8d1e6dd3d25fd49ae4f744baa4dc12fc" } } } diff --git a/src/dictionaries/abbrs.json b/src/dictionaries/abbrs.json new file mode 100644 index 0000000..6e871f2 --- /dev/null +++ b/src/dictionaries/abbrs.json @@ -0,0 +1,189 @@ +{ + "version": "0.1.0", + "abbreviations": [ + { + "word": "repository", + "abbr": "repo" + }, + { + "word": "repositories", + "abbr": "repos" + }, + { + "word": "configuration", + "abbr": "config" + }, + { + "word": "application", + "abbr": "app" + }, + { + "word": "applications", + "abbr": "apps" + }, + { + "word": "authentication", + "abbr": "auth" + }, + { + "word": "authorization", + "abbr": "authz" + }, + { + "word": "administrator", + "abbr": "admin" + }, + { + "word": "administrators", + "abbr": "admins" + }, + { + "word": "environment", + "abbr": "env" + }, + { + "word": "environments", + "abbr": "envs" + }, + { + "word": "organization", + "abbr": "org" + }, + { + "word": "organizations", + "abbr": "orgs" + }, + { + "word": "information", + "abbr": "info" + }, + { + "word": "description", + "abbr": "desc" + }, + { + "word": "specification", + "abbr": "spec" + }, + { + "word": "specifications", + "abbr": "specs" + }, + { + "word": "parameter", + "abbr": "param" + }, + { + "word": "parameters", + "abbr": "params" + }, + { + "word": "argument", + "abbr": "arg" + }, + { + "word": "arguments", + "abbr": "args" + }, + { + "word": "reference", + "abbr": "ref" + }, + { + "word": "references", + "abbr": "refs" + }, + { + "word": "attribute", + "abbr": "attr" + }, + { + "word": "attributes", + "abbr": "attrs" + }, + { + "word": "statistic", + "abbr": "stat" + }, + { + "word": "statistics", + "abbr": "stats" + }, + { + "word": "document", + "abbr": "doc" + }, + { + "word": "documents", + "abbr": "docs" + }, + { + "word": "message", + "abbr": "msg" + }, + { + "word": "messages", + "abbr": "msgs" + }, + { + "word": "database", + "abbr": "db" + }, + { + "word": "databases", + "abbr": "dbs" + }, + { + "word": "temporary", + "abbr": "temp" + }, + { + "word": "maximum", + "abbr": "max" + }, + { + "word": "minimum", + "abbr": "min" + }, + { + "word": "kubernetes", + "abbr": "k8s" + }, + { + "word": "internationalization", + "abbr": "i18n" + }, + { + "word": "localization", + "abbr": "l10n" + }, + { + "word": "metadata", + "abbr": "meta" + }, + { + "word": "identifier", + "abbr": "id" + }, + { + "word": "identifiers", + "abbr": "ids" + }, + { + "word": "variable", + "abbr": "var" + }, + { + "word": "variables", + "abbr": "vars" + }, + { + "word": "package", + "abbr": "pkg" + }, + { + "word": "packages", + "abbr": "pkgs" + } + ] +} diff --git a/src/headers.ts b/src/headers.ts index c4ce7c0..8904f59 100644 --- a/src/headers.ts +++ b/src/headers.ts @@ -138,8 +138,7 @@ export class OCPHeaders { const contextId = normalizedHeaders[OCP_CONTEXT_ID.toLowerCase()] || 'unknown'; const agentType = normalizedHeaders[OCP_AGENT_TYPE.toLowerCase()] || 'unknown'; const goal = normalizedHeaders[OCP_CURRENT_GOAL.toLowerCase()] || 'none'; - const user = normalizedHeaders[OCP_USER.toLowerCase()] || 'unknown'; - const workspace = normalizedHeaders[OCP_WORKSPACE.toLowerCase()] || 'unknown'; + const workspace = normalizedHeaders[OCP_WORKSPACE.toLowerCase()] || 'none'; return `OCP Context: ${contextId} | Agent: ${agentType} | Goal: ${goal} | Workspace: ${workspace}`; } @@ -163,7 +162,6 @@ export class OCPHeaders { OCP_SESSION.toLowerCase(), OCP_CURRENT_GOAL.toLowerCase(), OCP_AGENT_TYPE.toLowerCase(), - OCP_USER.toLowerCase(), OCP_WORKSPACE.toLowerCase(), OCP_VERSION.toLowerCase(), ]); diff --git a/src/http_client.ts b/src/http_client.ts index f203edc..decb074 100644 --- a/src/http_client.ts +++ b/src/http_client.ts @@ -54,8 +54,8 @@ export class OCPHTTPClient { const ocpHeaders = createOCPHeaders(this.context); return { - ...ocpHeaders, - ...additionalHeaders + ...additionalHeaders, + ...ocpHeaders }; } @@ -87,7 +87,7 @@ export class OCPHTTPClient { method: method.toUpperCase(), url: url, domain: parsedUrl.hostname, - success: !error && statusCode ? statusCode >= 200 && statusCode < 300 : false, + success: error ? false : (statusCode !== undefined ? statusCode >= 200 && statusCode < 300 : null), }; if (statusCode !== undefined) { diff --git a/src/parsers/inflector.ts b/src/parsers/inflector.ts new file mode 100644 index 0000000..0bac58e --- /dev/null +++ b/src/parsers/inflector.ts @@ -0,0 +1,160 @@ +/** + * English pluralization and singularization rules. + * + * Based on: https://github.com/weixu365/pluralizer-py + * Adapted for API path resource name inflection. + */ + +// Irregular word mappings (singular → plural) +export const IRREGULARS: [string, string][] = [ + ['person', 'people'], + ['man', 'men'], + ['human', 'humans'], + ['child', 'children'], + ['sex', 'sexes'], + ['move', 'moves'], + ['cow', 'kine'], + ['zombie', 'zombies'], + ['goose', 'geese'], + ['foot', 'feet'], + ['tooth', 'teeth'], + ['quiz', 'quizzes'], + ['ox', 'oxen'], + ['axe', 'axes'], + ['die', 'dice'], + ['yes', 'yeses'], + ['mouse', 'mice'], + ['datum', 'data'], + ['schema', 'schemata'], +]; + +// Build reverse mapping for singularization +export const IRREGULAR_SINGULAR_MAP: Record = Object.fromEntries(IRREGULARS); +export const IRREGULAR_PLURAL_MAP: Record = Object.fromEntries( + IRREGULARS.map(([s, p]) => [p, s]) +); + +// Uncountable words (never change) +export const UNCOUNTABLES: string[] = [ + 'equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', + 'sheep', 'moose', 'deer', 'news', 'audio', 'video', 'hardware', 'software', + 'firmware', 'metadata', 'data', 'analytics', 'media', 'traffic', 'mail', + 'police', 'cattle', 'staff', 'personnel', 'research', 'music', 'garbage', + 'chaos', 'evidence', 'furniture', 'luggage', 'machinery', 'wildlife', + 'health', 'wealth', 'history', +]; + +const UNCOUNTABLE_SET = new Set(UNCOUNTABLES); + +// Pluralization rules: [pattern, replacement] +const PLURALIZATION_RULES: [RegExp, string][] = [ + [/(quiz)$/i, '$1zes'], + [/^(oxen)$/i, '$1'], + [/^(ox)$/i, '$1en'], + [/(m|l)ice$/i, '$1ice'], + [/(m|l)ouse$/i, '$1ice'], + [/(matr|vert|ind)(?:ix|ex)$/i, '$1ices'], + [/(x|ch|ss|sh)$/i, '$1es'], + [/([^aeiouy]|qu)y$/i, '$1ies'], + [/(hive)$/i, '$1s'], + [/(?:([^f])fe|([lr])f)$/i, '$1$2ves'], + [/sis$/i, 'ses'], + [/([ti])a$/i, '$1a'], + [/([ti])um$/i, '$1a'], + [/(buffal|tomat|potat)o$/i, '$1oes'], + [/(bu)s$/i, '$1ses'], + [/(alias|status)$/i, '$1es'], + [/(octop|vir)us$/i, '$1i'], + [/(ax|test)is$/i, '$1es'], + [/s$/i, 's'], + [/$/, 's'], +]; + +// Singularization rules: [pattern, replacement] +const SINGULARIZATION_RULES: [RegExp, string][] = [ + [/(quiz)zes$/i, '$1'], + [/(matr)ices$/i, '$1ix'], + [/(vert|ind)ices$/i, '$1ex'], + [/^(ox)en/i, '$1'], + [/(alias|status)es$/i, '$1'], + [/(octop|vir)i$/i, '$1us'], + [/(cris|ax|test)es$/i, '$1is'], + [/(shoe)s$/i, '$1'], + [/(o)es$/i, '$1'], + [/(bus)es$/i, '$1'], + [/([m|l])ice$/i, '$1ouse'], + [/(x|ch|ss|sh)es$/i, '$1'], + [/(m)ovies$/i, '$1ovie'], + [/(s)eries$/i, '$1eries'], + [/([^aeiouy]|qu)ies$/i, '$1y'], + [/([lr])ves$/i, '$1f'], + [/(tive)s$/i, '$1'], + [/(hive)s$/i, '$1'], + [/([^f])ves$/i, '$1fe'], + [/(^analy)ses$/i, '$1sis'], + [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '$1$2sis'], + [/([ti])a$/i, '$1um'], + [/(n)ews$/i, '$1ews'], + [/s$/i, ''], +]; + +export function isUncountable(word: string): boolean { + return UNCOUNTABLE_SET.has(word.toLowerCase()); +} + +export function pluralize(word: string): string { + if (!word) return word; + + if (isUncountable(word)) return word; + + const wordLower = word.toLowerCase(); + if (wordLower in IRREGULAR_SINGULAR_MAP) { + const plural = IRREGULAR_SINGULAR_MAP[wordLower]; + if (word[0] === word[0].toUpperCase() && word[0] !== word[0].toLowerCase()) { + return plural.charAt(0).toUpperCase() + plural.slice(1); + } + return plural; + } + + for (const [pattern, replacement] of PLURALIZATION_RULES) { + if (pattern.test(word)) { + return word.replace(pattern, replacement); + } + } + + return word; +} + +export function singularize(word: string): string { + if (!word) return word; + + if (isUncountable(word)) return word; + + const wordLower = word.toLowerCase(); + if (wordLower in IRREGULAR_PLURAL_MAP) { + const singular = IRREGULAR_PLURAL_MAP[wordLower]; + if (word[0] === word[0].toUpperCase() && word[0] !== word[0].toLowerCase()) { + return singular.charAt(0).toUpperCase() + singular.slice(1); + } + return singular; + } + + for (const [pattern, replacement] of SINGULARIZATION_RULES) { + if (pattern.test(word)) { + return word.replace(pattern, replacement); + } + } + + return word; +} + +export function isPlural(word: string): boolean { + if (!word || isUncountable(word)) return false; + return singularize(word) !== word; +} + +export function isSingular(word: string): boolean { + if (!word || isUncountable(word)) return false; + if (word.toLowerCase() in IRREGULAR_PLURAL_MAP) return false; + return pluralize(word) !== word; +} diff --git a/src/parsers/openapi_parser.ts b/src/parsers/openapi_parser.ts index 028c112..71e71a8 100644 --- a/src/parsers/openapi_parser.ts +++ b/src/parsers/openapi_parser.ts @@ -4,6 +4,7 @@ import { APISpecParser, OCPAPISpec, OCPTool } from './base.js'; import { SchemaDiscoveryError } from '../errors.js'; +import { ToolNamingEngine } from './tool_naming.js'; // Configuration constants const DEFAULT_API_TITLE = 'Unknown API'; @@ -15,6 +16,7 @@ const SUPPORTED_HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete']; */ export class OpenAPIParser extends APISpecParser { private specVersion?: string; + private namingEngine = new ToolNamingEngine(); canParse(specData: Record): boolean { return 'swagger' in specData || 'openapi' in specData; @@ -27,7 +29,7 @@ export class OpenAPIParser extends APISpecParser { pathPrefix?: string ): OCPAPISpec { this.specVersion = this.detectSpecVersion(specData); - const apiSpec = this.parseOpenApiSpec(specData, baseUrlOverride); + const apiSpec = this.parseOpenApiSpec(specData, baseUrlOverride, pathPrefix); // Apply resource filtering if specified if (includeResources) { @@ -81,7 +83,7 @@ export class OpenAPIParser extends APISpecParser { /** * Parse OpenAPI specification and extract tools with lazy $ref resolution. */ - private parseOpenApiSpec(spec: Record, baseUrlOverride?: string): OCPAPISpec { + private parseOpenApiSpec(spec: Record, baseUrlOverride?: string, pathPrefix?: string): OCPAPISpec { // Initialize memoization cache for lazy $ref resolution const memoCache: Record = {}; @@ -109,7 +111,7 @@ export class OpenAPIParser extends APISpecParser { continue; } - const tool = this.createToolFromOperation(path, method, operation, spec, memoCache); + const tool = this.createToolFromOperation(path, method.toUpperCase(), operation, spec, memoCache, pathPrefix); if (tool) { tools.push(tool); } @@ -154,45 +156,29 @@ export class OpenAPIParser extends APISpecParser { * Create tool definition from OpenAPI operation. */ private createToolFromOperation( - path: string, - method: string, + path: string, + method: string, operation: Record, specData: Record, - memoCache: Record + memoCache: Record, + pathPrefix?: string ): OCPTool | null { if (!operation || typeof operation !== 'object') { return null; } - // Generate tool name with proper validation and fallback logic - const operationId = operation.operationId; - let toolName: string | null = null; - - // Try operationId first - if (operationId) { - const normalizedName = this.normalizeToolName(operationId); - if (this.isValidToolName(normalizedName)) { - toolName = normalizedName; - } - } - - // If operationId failed, try fallback naming - if (!toolName) { - // Generate name from path and method - const cleanPath = path.replace(/\//g, '_').replace(/[{}]/g, ''); - const fallbackName = `${method.toLowerCase()}${cleanPath}`; - const normalizedFallback = this.normalizeToolName(fallbackName); - if (this.isValidToolName(normalizedFallback)) { - toolName = normalizedFallback; - } - } - - // If we can't generate a valid tool name, skip this operation - if (!toolName) { + // Generate tool name from path and method using the naming engine + let toolName: string; + try { + toolName = this.namingEngine.generateToolName(path, method, pathPrefix); + } catch { console.warn(`Skipping operation ${method} ${path}: unable to generate valid tool name`); return null; } + // Store operation ID for reference + const operationId = operation.operationId || undefined; + const description = operation.summary || operation.description || 'No description provided'; const tags = operation.tags || []; @@ -200,7 +186,7 @@ export class OpenAPIParser extends APISpecParser { const parameters = this.parseParameters(operation.parameters || [], specData, memoCache); // Add request body parameters (version-specific) - if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { + if (['POST', 'PUT', 'PATCH'].includes(method)) { if (this.specVersion === 'swagger_2') { // Swagger 2.0: body is in parameters array for (const param of (operation.parameters || [])) { @@ -222,7 +208,7 @@ export class OpenAPIParser extends APISpecParser { return { name: toolName, description, - method: method.toUpperCase(), + method: method, path, parameters, response_schema: responseSchema, @@ -231,59 +217,6 @@ export class OpenAPIParser extends APISpecParser { }; } - /** - * Normalize tool name to camelCase, removing special characters. - */ - private normalizeToolName(name: string): string { - if (!name) { - return name; - } - - // First, split PascalCase/camelCase words (e.g., "FetchAccount" -> "Fetch Account") - // Insert space before uppercase letters that follow lowercase letters or digits - const pascalSplit = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); - - // Replace separators (/, _, -, .) with spaces for processing - // Also handle multiple consecutive separators like // - const normalized = pascalSplit.replace(/[\/_.-]+/g, ' '); - - // Split into words and filter out empty strings - const words = normalized.split(' ').filter(word => word); - - if (words.length === 0) { - return name; - } - - // Convert to camelCase: first word lowercase, rest capitalize - const camelCaseWords = [words[0].toLowerCase()]; - for (let i = 1; i < words.length; i++) { - camelCaseWords.push(words[i].charAt(0).toUpperCase() + words[i].slice(1).toLowerCase()); - } - - return camelCaseWords.join(''); - } - - /** - * Check if a normalized tool name is valid. - */ - private isValidToolName(name: string): boolean { - if (!name) { - return false; - } - - // Must start with a letter - if (!/^[a-zA-Z]/.test(name)) { - return false; - } - - // Must contain at least one alphanumeric character - if (!/[a-zA-Z0-9]/.test(name)) { - return false; - } - - return true; - } - /** * Parse OpenAPI parameters with lazy $ref resolution. */ diff --git a/src/parsers/tool_naming.ts b/src/parsers/tool_naming.ts new file mode 100644 index 0000000..08f4581 --- /dev/null +++ b/src/parsers/tool_naming.ts @@ -0,0 +1,312 @@ +/** + * Tool naming engine for generating semantic, deterministic tool names from REST API paths. + * + * This module is format-agnostic and shared across all spec parsers (OpenAPI, Google Discovery, etc.). + * It takes raw REST paths and generates consistent camelCase tool names. + */ + +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import * as inflector from './inflector.js'; + +// Action verbs that are redundant with HTTP methods. +// These are stripped from path segments (e.g., /calls.participants.add → /calls/participants) +export const ACTION_VERBS: Set = new Set([ + 'add', 'remove', 'create', 'delete', 'get', 'list', 'update', 'set', + 'post', 'archive', 'unarchive', 'enable', 'disable', 'open', 'close', + 'invite', 'kick', 'join', 'leave', 'mark', 'unmark', 'end', 'publish', + 'upload', 'share', 'complete', 'rename', 'revoke', 'fetch', 'put', 'patch', +]); + +interface PathComponent { + type: 'RESOURCE' | 'PARAM'; + value: string; +} + +/** + * Generates tool names from REST API paths using a deterministic algorithm. + */ +export class ToolNamingEngine { + abbreviations: Record; + + constructor(abbreviations?: Record) { + this.abbreviations = abbreviations ?? this.loadAbbreviations(); + } + + private loadAbbreviations(): Record { + try { + let abbrPath: string; + if (typeof __dirname !== 'undefined') { + // CommonJS context + abbrPath = join(__dirname, '../dictionaries/abbrs.json'); + } else { + // ESM context + const __filename = fileURLToPath(import.meta.url); + abbrPath = join(dirname(__filename), '../dictionaries/abbrs.json'); + } + const data = JSON.parse(readFileSync(abbrPath, 'utf8')); + return Object.fromEntries( + data.abbreviations.map((item: { word: string; abbr: string }) => [item.word, item.abbr]) + ); + } catch { + return {}; + } + } + + /** + * Clean path by stripping prefix and file extensions. + */ + cleanPath(path: string, pathPrefix?: string): string { + let cleaned = path; + + if (pathPrefix) { + const prefixLower = pathPrefix.toLowerCase(); + const pathLower = path.toLowerCase(); + if (pathLower.startsWith(prefixLower)) { + cleaned = path.slice(pathPrefix.length); + } + } + + cleaned = cleaned.replace(/\.(json|xml|yaml|yml)$/, ''); + + return cleaned; + } + + /** + * Parse path into structured components (RESOURCE or PARAM). + */ + parsePathComponents(path: string): PathComponent[] { + const components: PathComponent[] = []; + + const segments: string[] = []; + for (const slashSegment of path.split('/')) { + if (slashSegment) { + const dotSegments = slashSegment.split('.').filter(s => s); + segments.push(...dotSegments); + } + } + + for (const segment of segments) { + if (segment.startsWith('{') && segment.endsWith('}')) { + components.push({ type: 'PARAM', value: segment.slice(1, -1) }); + } else { + components.push({ type: 'RESOURCE', value: segment }); + } + } + + return components; + } + + /** + * Identify container resources to skip. + * A resource is a container if there are 2 or more resources after its parameters. + */ + detectContainerResources(components: PathComponent[]): string[] { + const containers: string[] = []; + + for (let i = 0; i < components.length; i++) { + const component = components[i]; + if (component.type !== 'RESOURCE') continue; + + let resourcesAfterParams = 0; + let foundParam = false; + + for (let j = i + 1; j < components.length; j++) { + if (components[j].type === 'PARAM') { + foundParam = true; + } else if (components[j].type === 'RESOURCE') { + if (foundParam) { + resourcesAfterParams++; + } + } + } + + if (resourcesAfterParams >= 2) { + containers.push(component.value); + } + } + + return containers; + } + + /** + * Extract resource names, excluding containers and stripping action verbs. + */ + extractResourceChain(components: PathComponent[], containers: string[]): string[] { + const resources: string[] = []; + + for (const component of components) { + if (component.type === 'RESOURCE') { + let resourceName = component.value; + if (!containers.includes(resourceName)) { + resourceName = this.stripActionVerb(resourceName); + if (resourceName) { + resources.push(resourceName); + } + } + } + } + + return resources; + } + + /** + * Strip action verb from a resource name. + */ + stripActionVerb(name: string): string { + const nameLower = name.toLowerCase(); + + if (ACTION_VERBS.has(nameLower)) { + return ''; + } + + for (const verb of ACTION_VERBS) { + if (nameLower.startsWith(verb) && name.length > verb.length) { + const rest = name.slice(verb.length); + if (rest[0] === rest[0].toUpperCase() && rest[0] !== rest[0].toLowerCase()) { + return rest; + } + } + } + + return name; + } + + /** + * Generate a tool name from a REST API path. + */ + generateToolName(path: string, method: string, pathPrefix?: string): string { + // Step 1: Strip path prefix and file extensions + const cleanedPath = this.cleanPath(path, pathPrefix); + + // Step 2: Parse path into components + const components = this.parsePathComponents(cleanedPath); + + if (components.length === 0) { + throw new Error(`Cannot generate tool name: no path components in '${path}'`); + } + + // Step 3: Detect container resources + const containers = this.detectContainerResources(components); + + // Step 4: Extract resource chain + const resources = this.extractResourceChain(components, containers); + + if (resources.length === 0) { + throw new Error(`Cannot generate tool name: no resources found in '${path}'`); + } + + // Step 5: Determine if this is a collection endpoint + const isCollection = components.length > 0 && components[components.length - 1].type === 'RESOURCE'; + + // Step 6: Determine verb + const verb = this.determineVerb(method, isCollection); + + // Step 7: Apply singularization + const inflected = this.applySingularization(resources, verb); + + // Step 8: Apply abbreviations + const abbreviated = this.applyAbbreviations(inflected); + + // Step 9: Build camelCase name + const toolName = this.buildCamelCaseName(verb, abbreviated); + + // Step 10: Validate + if (!this.isValidToolName(toolName)) { + throw new Error(`Generated invalid tool name: ${toolName}`); + } + + return toolName; + } + + /** + * Determine the appropriate verb based on HTTP method and endpoint type. + */ + determineVerb(method: string, isCollection: boolean): string { + switch (method.toUpperCase()) { + case 'GET': + return isCollection ? 'list' : 'get'; + case 'POST': + return 'create'; + case 'PUT': + case 'PATCH': + return 'update'; + case 'DELETE': + return 'delete'; + default: + return method.toLowerCase(); + } + } + + /** + * Apply inflection rules to resource chain. + * - All non-final resources: always singular + * - Final resource: plural for 'list', singular for everything else + */ + applySingularization(resources: string[], verb: string): string[] { + if (resources.length === 0) return resources; + + const inflected = resources.slice(0, -1).map(r => inflector.singularize(r)); + + const lastResource = resources[resources.length - 1]; + if (verb === 'list') { + inflected.push(inflector.pluralize(lastResource)); + } else { + inflected.push(inflector.singularize(lastResource)); + } + + return inflected; + } + + /** + * Apply abbreviations from dictionary to resource names. + */ + applyAbbreviations(resources: string[]): string[] { + return resources.map(resource => { + const key = resource.toLowerCase(); + return key in this.abbreviations ? this.abbreviations[key] : resource; + }); + } + + /** + * Build camelCase tool name from verb and resources. + */ + buildCamelCaseName(verb: string, resources: string[]): string { + const parts = [verb.toLowerCase()]; + for (const resource of resources) { + parts.push(this.normalizeToCamelCase(resource)); + } + return parts.join(''); + } + + /** + * Normalize a name to PascalCase (first letter capitalized). + * Handles kebab-case, snake_case, and PascalCase/camelCase input. + */ + normalizeToCamelCase(name: string): string { + if (!name) return name; + + // Split PascalCase/camelCase boundaries + const pascalSplit = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); + + // Replace separators with spaces + const normalized = pascalSplit.replace(/[\/_.\-]+/g, ' '); + + const words = normalized.split(' ').filter(w => w); + + if (words.length === 0) return name; + + return words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(''); + } + + /** + * Validate that a tool name meets requirements. + */ + isValidToolName(name: string): boolean { + if (!name) return false; + if (!/^[a-zA-Z]/.test(name)) return false; + if (!/[a-zA-Z0-9]/.test(name)) return false; + return true; + } +} diff --git a/tests/fixtures/test_spec.json b/tests/fixtures/test_spec.json index c2935ce..fdbaced 100644 --- a/tests/fixtures/test_spec.json +++ b/tests/fixtures/test_spec.json @@ -12,7 +12,6 @@ "paths": { "/test": { "get": { - "operationId": "getTest", "summary": "Test operation", "responses": { "200": { diff --git a/tests/fixtures/test_spec.yaml b/tests/fixtures/test_spec.yaml index 590bb7f..d892186 100644 --- a/tests/fixtures/test_spec.yaml +++ b/tests/fixtures/test_spec.yaml @@ -7,7 +7,6 @@ servers: paths: /yaml-test: get: - operationId: getYamlTest summary: YAML test endpoint responses: '200': diff --git a/tests/http_client.test.ts b/tests/http_client.test.ts index 6884f28..20a9e12 100644 --- a/tests/http_client.test.ts +++ b/tests/http_client.test.ts @@ -28,10 +28,8 @@ describe('OCP HTTP Client', () => { }); test('init auto update context false', () => { - // Note: JavaScript implementation doesn't have auto_update_context parameter yet - // This test would need to be updated if that feature is added - const ocpClient = new OCPHTTPClient(context); - expect(ocpClient).toBeDefined(); + const ocpClient = new OCPHTTPClient(context, false); + expect(ocpClient['autoUpdateContext']).toBe(false); }); }); @@ -69,8 +67,8 @@ describe('OCP HTTP Client', () => { const headers = ocpClient['_prepareHeaders'](existingHeaders); // OCP header should actually override existing header (based on implementation) - // The implementation does: {...ocpHeaders, ...additionalHeaders} which means additional wins - expect(headers[ocpHeaderKey]).toBe('overridden_value'); + // The implementation does: {...additionalHeaders, ...ocpHeaders} which means OCP wins + expect(headers[ocpHeaderKey]).toBe(ocpHeaders[ocpHeaderKey]); }); }); @@ -168,9 +166,11 @@ describe('OCP HTTP Client', () => { method: 'PUT', url: 'https://api.example.com/update', domain: 'api.example.com', - success: false, + success: null, } - ); addInteractionSpy.mockRestore(); + ); + + addInteractionSpy.mockRestore(); }); }); diff --git a/tests/inflector.test.ts b/tests/inflector.test.ts new file mode 100644 index 0000000..d549e4b --- /dev/null +++ b/tests/inflector.test.ts @@ -0,0 +1,328 @@ +/** + * Tests for inflector module - English pluralization and singularization. + */ + +import { + pluralize, + singularize, + isPlural, + isSingular, + isUncountable, + UNCOUNTABLES, + IRREGULARS, +} from '../src/parsers/inflector.js'; + +describe('pluralize', () => { + test('regular words', () => { + expect(pluralize('repository')).toBe('repositories'); + expect(pluralize('dashboard')).toBe('dashboards'); + expect(pluralize('user')).toBe('users'); + expect(pluralize('issue')).toBe('issues'); + expect(pluralize('comment')).toBe('comments'); + expect(pluralize('project')).toBe('projects'); + }); + + test('words ending in s, x, z, ch, sh', () => { + expect(pluralize('class')).toBe('classes'); + expect(pluralize('box')).toBe('boxes'); + expect(pluralize('quiz')).toBe('quizzes'); + expect(pluralize('church')).toBe('churches'); + expect(pluralize('dish')).toBe('dishes'); + expect(pluralize('bus')).toBe('buses'); + }); + + test('words ending in y', () => { + expect(pluralize('category')).toBe('categories'); + expect(pluralize('company')).toBe('companies'); + expect(pluralize('query')).toBe('queries'); + // But not when preceded by vowel + expect(pluralize('key')).toBe('keys'); + expect(pluralize('boy')).toBe('boys'); + }); + + test('words ending in f or fe', () => { + expect(pluralize('knife')).toBe('knives'); + expect(pluralize('life')).toBe('lives'); + expect(pluralize('wolf')).toBe('wolves'); + }); + + test('words ending in o', () => { + expect(pluralize('tomato')).toBe('tomatoes'); + expect(pluralize('potato')).toBe('potatoes'); + expect(pluralize('buffalo')).toBe('buffaloes'); + }); + + test('irregular words', () => { + expect(pluralize('person')).toBe('people'); + expect(pluralize('man')).toBe('men'); + expect(pluralize('child')).toBe('children'); + expect(pluralize('mouse')).toBe('mice'); + expect(pluralize('goose')).toBe('geese'); + expect(pluralize('foot')).toBe('feet'); + expect(pluralize('tooth')).toBe('teeth'); + expect(pluralize('ox')).toBe('oxen'); + }); + + test('uncountable words remain unchanged', () => { + expect(pluralize('data')).toBe('data'); + expect(pluralize('information')).toBe('information'); + expect(pluralize('equipment')).toBe('equipment'); + expect(pluralize('metadata')).toBe('metadata'); + expect(pluralize('software')).toBe('software'); + expect(pluralize('history')).toBe('history'); + }); + + test('capitalization preserved', () => { + expect(pluralize('Repository')).toBe('Repositories'); + expect(pluralize('Person')).toBe('People'); + expect(pluralize('User')).toBe('Users'); + }); + + test('empty string returns empty', () => { + expect(pluralize('')).toBe(''); + }); + + test('already plural words', () => { + expect(pluralize('users')).toBe('users'); + expect(pluralize('repositories')).toBe('repositories'); + }); +}); + +describe('singularize', () => { + test('regular words', () => { + expect(singularize('repositories')).toBe('repository'); + expect(singularize('dashboards')).toBe('dashboard'); + expect(singularize('users')).toBe('user'); + expect(singularize('issues')).toBe('issue'); + expect(singularize('comments')).toBe('comment'); + expect(singularize('projects')).toBe('project'); + }); + + test('words ending in es', () => { + expect(singularize('classes')).toBe('class'); + expect(singularize('boxes')).toBe('box'); + expect(singularize('quizzes')).toBe('quiz'); + expect(singularize('churches')).toBe('church'); + expect(singularize('dishes')).toBe('dish'); + expect(singularize('buses')).toBe('bus'); + }); + + test('words ending in ies', () => { + expect(singularize('categories')).toBe('category'); + expect(singularize('companies')).toBe('company'); + expect(singularize('queries')).toBe('query'); + }); + + test('words ending in ves', () => { + expect(singularize('knives')).toBe('knife'); + expect(singularize('lives')).toBe('life'); + expect(singularize('wolves')).toBe('wolf'); + }); + + test('words ending in oes', () => { + expect(singularize('tomatoes')).toBe('tomato'); + expect(singularize('potatoes')).toBe('potato'); + }); + + test('irregular words', () => { + expect(singularize('people')).toBe('person'); + expect(singularize('men')).toBe('man'); + expect(singularize('children')).toBe('child'); + expect(singularize('mice')).toBe('mouse'); + expect(singularize('geese')).toBe('goose'); + expect(singularize('feet')).toBe('foot'); + expect(singularize('teeth')).toBe('tooth'); + expect(singularize('oxen')).toBe('ox'); + }); + + test('uncountable words remain unchanged', () => { + expect(singularize('data')).toBe('data'); + expect(singularize('information')).toBe('information'); + expect(singularize('equipment')).toBe('equipment'); + expect(singularize('metadata')).toBe('metadata'); + expect(singularize('software')).toBe('software'); + expect(singularize('history')).toBe('history'); + }); + + test('capitalization preserved', () => { + expect(singularize('Repositories')).toBe('Repository'); + expect(singularize('People')).toBe('Person'); + expect(singularize('Users')).toBe('User'); + }); + + test('empty string returns empty', () => { + expect(singularize('')).toBe(''); + }); + + test('already singular words', () => { + expect(singularize('user')).toBe('user'); + expect(singularize('repository')).toBe('repository'); + }); +}); + +describe('isPlural', () => { + test('plural words are identified correctly', () => { + expect(isPlural('users')).toBe(true); + expect(isPlural('repositories')).toBe(true); + expect(isPlural('categories')).toBe(true); + expect(isPlural('people')).toBe(true); + expect(isPlural('children')).toBe(true); + }); + + test('singular words return false', () => { + expect(isPlural('user')).toBe(false); + expect(isPlural('repository')).toBe(false); + expect(isPlural('category')).toBe(false); + expect(isPlural('person')).toBe(false); + expect(isPlural('child')).toBe(false); + }); + + test('uncountable words return false', () => { + expect(isPlural('data')).toBe(false); + expect(isPlural('information')).toBe(false); + expect(isPlural('equipment')).toBe(false); + }); + + test('empty string returns false', () => { + expect(isPlural('')).toBe(false); + }); + + test('edge cases', () => { + expect(isPlural('news')).toBe(false); // uncountable + expect(isPlural('series')).toBe(false); // uncountable + expect(isPlural('species')).toBe(false); // uncountable + }); +}); + +describe('isSingular', () => { + test('singular words are identified correctly', () => { + expect(isSingular('user')).toBe(true); + expect(isSingular('repository')).toBe(true); + expect(isSingular('category')).toBe(true); + expect(isSingular('person')).toBe(true); + expect(isSingular('child')).toBe(true); + }); + + test('plural words return false', () => { + expect(isSingular('users')).toBe(false); + expect(isSingular('repositories')).toBe(false); + expect(isSingular('categories')).toBe(false); + expect(isSingular('people')).toBe(false); + expect(isSingular('children')).toBe(false); + }); + + test('uncountable words return false', () => { + expect(isSingular('data')).toBe(false); + expect(isSingular('information')).toBe(false); + expect(isSingular('equipment')).toBe(false); + }); + + test('empty string returns false', () => { + expect(isSingular('')).toBe(false); + }); + + test('edge cases', () => { + expect(isSingular('news')).toBe(false); // uncountable + expect(isSingular('series')).toBe(false); // uncountable + expect(isSingular('species')).toBe(false); // uncountable + }); +}); + +describe('isUncountable', () => { + test('uncountable words are identified', () => { + expect(isUncountable('data')).toBe(true); + expect(isUncountable('information')).toBe(true); + expect(isUncountable('equipment')).toBe(true); + expect(isUncountable('metadata')).toBe(true); + expect(isUncountable('history')).toBe(true); + expect(isUncountable('news')).toBe(true); + expect(isUncountable('series')).toBe(true); + }); + + test('countable words return false', () => { + expect(isUncountable('user')).toBe(false); + expect(isUncountable('repository')).toBe(false); + expect(isUncountable('category')).toBe(false); + }); + + test('check is case-insensitive', () => { + expect(isUncountable('Data')).toBe(true); + expect(isUncountable('DATA')).toBe(true); + expect(isUncountable('Information')).toBe(true); + }); +}); + +describe('round-trip conversions', () => { + test('singular → plural → singular', () => { + const words = ['user', 'repository', 'category', 'person', 'child']; + for (const word of words) { + const plural = pluralize(word); + const backToSingular = singularize(plural); + expect(backToSingular).toBe(word); + } + }); + + test('plural → singular → plural', () => { + const words = ['users', 'repositories', 'categories', 'people', 'children']; + for (const word of words) { + const singular = singularize(word); + const backToPlural = pluralize(singular); + expect(backToPlural).toBe(word); + } + }); +}); + +describe('API-specific words', () => { + test('common API resource names', () => { + expect(singularize('repos')).toBe('repo'); + expect(singularize('issues')).toBe('issue'); + expect(singularize('gists')).toBe('gist'); + expect(singularize('orgs')).toBe('org'); + expect(singularize('webhooks')).toBe('webhook'); + expect(singularize('endpoints')).toBe('endpoint'); + expect(singularize('branches')).toBe('branch'); + + expect(pluralize('repo')).toBe('repos'); + expect(pluralize('issue')).toBe('issues'); + expect(pluralize('gist')).toBe('gists'); + expect(pluralize('org')).toBe('orgs'); + expect(pluralize('webhook')).toBe('webhooks'); + expect(pluralize('endpoint')).toBe('endpoints'); + expect(pluralize('branch')).toBe('branches'); + }); + + test('tech terms', () => { + expect(singularize('analyses')).toBe('analysis'); + expect(pluralize('analysis')).toBe('analyses'); + + expect(pluralize('metadata')).toBe('metadata'); + expect(singularize('metadata')).toBe('metadata'); + expect(pluralize('data')).toBe('data'); + expect(singularize('data')).toBe('data'); + }); +}); + +describe('edge cases', () => { + test('words ending in ss', () => { + expect(pluralize('class')).toBe('classes'); + expect(singularize('classes')).toBe('class'); + expect(pluralize('pass')).toBe('passes'); + expect(singularize('passes')).toBe('pass'); + }); + + test('acronyms and abbreviations', () => { + expect(pluralize('API')).toBe('APIs'); + expect(pluralize('URL')).toBe('URLs'); + expect(pluralize('ID')).toBe('IDs'); + }); + + test('compound words', () => { + expect(pluralize('webhook')).toBe('webhooks'); + expect(singularize('webhooks')).toBe('webhook'); + }); + + test('words with numbers', () => { + expect(pluralize('v2endpoint')).toBe('v2endpoints'); + expect(singularize('v2endpoints')).toBe('v2endpoint'); + }); +}); diff --git a/tests/openapi_parser.test.ts b/tests/openapi_parser.test.ts index 5f90d08..31971aa 100644 --- a/tests/openapi_parser.test.ts +++ b/tests/openapi_parser.test.ts @@ -233,38 +233,6 @@ describe('OpenAPIParser', () => { expect(postTool!.parameters['name']['location']).toBe('body'); }); - // Tool name normalization tests - test('normalize tool name slash separators', () => { - expect(parser['normalizeToolName']('meta/root')).toBe('metaRoot'); - expect(parser['normalizeToolName']('repos/disable-vulnerability-alerts')).toBe('reposDisableVulnerabilityAlerts'); - }); - - test('normalize tool name underscore separators', () => { - expect(parser['normalizeToolName']('admin_apps_approve')).toBe('adminAppsApprove'); - expect(parser['normalizeToolName']('get_users_list')).toBe('getUsersList'); - }); - - test('normalize tool name pascal case', () => { - expect(parser['normalizeToolName']('FetchAccount')).toBe('fetchAccount'); - expect(parser['normalizeToolName']('GetUserProfile')).toBe('getUserProfile'); - }); - - test('normalize tool name numbers', () => { - expect(parser['normalizeToolName']('v2010/Accounts')).toBe('v2010Accounts'); - }); - - test('normalize tool name acronyms', () => { - expect(parser['normalizeToolName']('SMS/send')).toBe('smsSend'); - }); - - test('valid tool name', () => { - expect(parser['isValidToolName']('metaRoot')).toBe(true); - expect(parser['isValidToolName']('getUsersList')).toBe(true); - expect(parser['isValidToolName']('')).toBe(false); - expect(parser['isValidToolName']('123invalid')).toBe(false); - expect(parser['isValidToolName']('___')).toBe(false); - }); - // Resource filtering tests test('filter tools by resources', () => { const tools: OCPTool[] = [ diff --git a/tests/schema_discovery.test.ts b/tests/schema_discovery.test.ts index f93ffb2..3828357 100644 --- a/tests/schema_discovery.test.ts +++ b/tests/schema_discovery.test.ts @@ -180,7 +180,6 @@ describe('OCP Schema Discovery', () => { paths: { '/queue': { post: { - operationId: 'updateQueue', summary: 'Update queue', responses: { '200': { @@ -223,7 +222,7 @@ describe('OCP Schema Discovery', () => { const tool = apiSpec.tools[0]; // Verify the $ref was resolved - expect(tool.name).toBe('updateQueue'); + expect(tool.name).toBe('createQueue'); expect(tool.response_schema).toBeDefined(); expect(tool.response_schema?.type).toBe('object'); expect(tool.response_schema?.properties).toBeDefined(); @@ -243,7 +242,6 @@ describe('OCP Schema Discovery', () => { paths: { '/node': { get: { - operationId: 'getNode', summary: 'Get node', responses: { '200': { @@ -313,7 +311,6 @@ describe('OCP Schema Discovery', () => { paths: { '/payment': { get: { - operationId: 'getPayment', summary: 'Get payment', responses: { '200': { @@ -340,7 +337,7 @@ describe('OCP Schema Discovery', () => { status: { anyOf: [ { type: 'string' }, - { type: 'number' }, + { type: 'integer' }, ], }, source: { @@ -394,11 +391,11 @@ describe('OCP Schema Discovery', () => { expect(tool.response_schema?.type).toBe('object'); expect(tool.response_schema?.properties).toBeDefined(); - // Status field with anyOf should have string and number types + // Status field with anyOf should have string and integer types const statusSchema = tool?.response_schema?.properties?.status; expect(statusSchema?.anyOf).toBeDefined(); expect(statusSchema?.anyOf[0]).toEqual({ type: 'string' }); - expect(statusSchema?.anyOf[1]).toEqual({ type: 'number' }); + expect(statusSchema?.anyOf[1]).toEqual({ type: 'integer' }); // Should not contain any $refs expect(JSON.stringify(statusSchema)).not.toContain('$ref'); @@ -521,6 +518,7 @@ describe('OCP Schema Discovery', () => { expect(doc).toContain('email'); expect(doc).toContain('age'); expect(doc.toLowerCase()).toContain('required'); + expect(doc.toLowerCase()).toContain('optional'); }); }); @@ -598,7 +596,7 @@ describe('OCP Schema Discovery', () => { expect(apiSpec.version).toBe('1.0.0'); expect(apiSpec.base_url).toBe('https://api.example.com'); expect(apiSpec.tools.length).toBe(1); - expect(apiSpec.tools[0].name).toBe('getTest'); + expect(apiSpec.tools[0].name).toBe('listTests'); }); test('load spec from relative path (JSON)', async () => { @@ -616,7 +614,7 @@ describe('OCP Schema Discovery', () => { expect(apiSpec.title).toBe('Test API from YAML'); expect(apiSpec.version).toBe('1.0.0'); expect(apiSpec.tools.length).toBe(1); - expect(apiSpec.tools[0].name).toBe('getYamlTest'); + expect(apiSpec.tools[0].name).toBe('listYamlTests'); }); test('error on file not found', async () => { diff --git a/tests/tool_naming.test.ts b/tests/tool_naming.test.ts new file mode 100644 index 0000000..e0cbf34 --- /dev/null +++ b/tests/tool_naming.test.ts @@ -0,0 +1,478 @@ +/** + * Tests for tool naming engine - REST path to tool name generation. + */ + +import { ToolNamingEngine, ACTION_VERBS } from '../src/parsers/tool_naming.js'; + +describe('ToolNamingEngine initialization', () => { + test('loads default abbreviations', () => { + const engine = new ToolNamingEngine(); + expect(engine.abbreviations).not.toBeNull(); + expect(typeof engine.abbreviations).toBe('object'); + expect('application' in engine.abbreviations).toBe(true); + expect(engine.abbreviations['application']).toBe('app'); + }); + + test('custom abbreviations', () => { + const custom = { repository: 'repo', organization: 'org' }; + const engine = new ToolNamingEngine(custom); + expect(engine.abbreviations).toEqual(custom); + }); + + test('empty abbreviations dict', () => { + const engine = new ToolNamingEngine({}); + expect(engine.abbreviations).toEqual({}); + }); +}); + +describe('cleanPath', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('strips path prefixes', () => { + expect(engine.cleanPath('/v1/balance', '/v1')).toBe('/balance'); + expect(engine.cleanPath('/rest/api/3/issue', '/rest/api/3')).toBe('/issue'); + expect(engine.cleanPath('/api/users', '/api')).toBe('/users'); + }); + + test('case-insensitive prefix stripping', () => { + expect(engine.cleanPath('/V1/balance', '/v1')).toBe('/balance'); + expect(engine.cleanPath('/v1/balance', '/V1')).toBe('/balance'); + expect(engine.cleanPath('/API/users', '/api')).toBe('/users'); + }); + + test('strips file extensions', () => { + expect(engine.cleanPath('/Accounts.json')).toBe('/Accounts'); + expect(engine.cleanPath('/Messages.xml')).toBe('/Messages'); + expect(engine.cleanPath('/config.yaml')).toBe('/config'); + expect(engine.cleanPath('/data.yml')).toBe('/data'); + }); + + test('strips both prefix and extension', () => { + expect(engine.cleanPath('/2010-04-01/Accounts/{Sid}/Calls.json', '/2010-04-01/Accounts/{Sid}')).toBe('/Calls'); + }); + + test('no prefix', () => { + expect(engine.cleanPath('/users')).toBe('/users'); + expect(engine.cleanPath('/repos/{id}')).toBe('/repos/{id}'); + }); + + test('empty path', () => { + expect(engine.cleanPath('')).toBe(''); + }); +}); + +describe('parsePathComponents', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('simple path', () => { + expect(engine.parsePathComponents('/users')).toEqual([ + { type: 'RESOURCE', value: 'users' } + ]); + }); + + test('path with param', () => { + expect(engine.parsePathComponents('/users/{id}')).toEqual([ + { type: 'RESOURCE', value: 'users' }, + { type: 'PARAM', value: 'id' } + ]); + }); + + test('nested path', () => { + expect(engine.parsePathComponents('/repos/{owner}/{repo}/issues')).toEqual([ + { type: 'RESOURCE', value: 'repos' }, + { type: 'PARAM', value: 'owner' }, + { type: 'PARAM', value: 'repo' }, + { type: 'RESOURCE', value: 'issues' } + ]); + }); + + test('dot notation (Slack-style)', () => { + expect(engine.parsePathComponents('/chat.postMessage')).toEqual([ + { type: 'RESOURCE', value: 'chat' }, + { type: 'RESOURCE', value: 'postMessage' } + ]); + + expect(engine.parsePathComponents('/calls.participants.add')).toEqual([ + { type: 'RESOURCE', value: 'calls' }, + { type: 'RESOURCE', value: 'participants' }, + { type: 'RESOURCE', value: 'add' } + ]); + }); + + test('mixed separators', () => { + expect(engine.parsePathComponents('/api/conversations.history')).toEqual([ + { type: 'RESOURCE', value: 'api' }, + { type: 'RESOURCE', value: 'conversations' }, + { type: 'RESOURCE', value: 'history' } + ]); + }); + + test('empty path', () => { + expect(engine.parsePathComponents('/')).toEqual([]); + }); +}); + +describe('detectContainerResources', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('basic container detection', () => { + const components = [ + { type: 'RESOURCE' as const, value: 'repos' }, + { type: 'PARAM' as const, value: 'owner' }, + { type: 'PARAM' as const, value: 'repo' }, + { type: 'RESOURCE' as const, value: 'codespaces' }, + { type: 'RESOURCE' as const, value: 'devcontainers' } + ]; + const containers = engine.detectContainerResources(components); + expect(containers).toContain('repos'); + expect(containers).not.toContain('codespaces'); + }); + + test('no containers', () => { + const components = [ + { type: 'RESOURCE' as const, value: 'users' }, + { type: 'PARAM' as const, value: 'id' } + ]; + expect(engine.detectContainerResources(components)).toHaveLength(0); + }); + + test('multi-level hierarchy', () => { + const components = [ + { type: 'RESOURCE' as const, value: 'orgs' }, + { type: 'PARAM' as const, value: 'org' }, + { type: 'RESOURCE' as const, value: 'repos' }, + { type: 'PARAM' as const, value: 'repo' }, + { type: 'RESOURCE' as const, value: 'issues' } + ]; + const containers = engine.detectContainerResources(components); + expect(containers).toContain('orgs'); + expect(containers).not.toContain('repos'); + }); +}); + +describe('stripActionVerb', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('pure action verbs', () => { + expect(engine.stripActionVerb('add')).toBe(''); + expect(engine.stripActionVerb('remove')).toBe(''); + expect(engine.stripActionVerb('create')).toBe(''); + expect(engine.stripActionVerb('delete')).toBe(''); + expect(engine.stripActionVerb('list')).toBe(''); + }); + + test('camelCase action verb prefixes', () => { + expect(engine.stripActionVerb('postMessage')).toBe('Message'); + expect(engine.stripActionVerb('getMessage')).toBe('Message'); + expect(engine.stripActionVerb('createAccount')).toBe('Account'); + expect(engine.stripActionVerb('deleteScheduledMessage')).toBe('ScheduledMessage'); + expect(engine.stripActionVerb('updateQueue')).toBe('Queue'); + }); + + test('no action verb present', () => { + expect(engine.stripActionVerb('history')).toBe('history'); + expect(engine.stripActionVerb('participants')).toBe('participants'); + expect(engine.stripActionVerb('message')).toBe('message'); + expect(engine.stripActionVerb('account')).toBe('account'); + }); + + test('case-insensitive verb detection', () => { + expect(engine.stripActionVerb('postMessage')).toBe('Message'); + expect(engine.stripActionVerb('PostMessage')).toBe('Message'); + expect(engine.stripActionVerb('POST')).toBe(''); + }); +}); + +describe('extractResourceChain', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('simple chain', () => { + const components = [ + { type: 'RESOURCE' as const, value: 'users' }, + { type: 'PARAM' as const, value: 'id' } + ]; + expect(engine.extractResourceChain(components, [])).toEqual(['users']); + }); + + test('excludes containers', () => { + const components = [ + { type: 'RESOURCE' as const, value: 'repos' }, + { type: 'PARAM' as const, value: 'owner' }, + { type: 'RESOURCE' as const, value: 'issues' } + ]; + expect(engine.extractResourceChain(components, ['repos'])).toEqual(['issues']); + }); + + test('strips action verbs', () => { + const components = [ + { type: 'RESOURCE' as const, value: 'calls' }, + { type: 'RESOURCE' as const, value: 'participants' }, + { type: 'RESOURCE' as const, value: 'add' } + ]; + const resources = engine.extractResourceChain(components, []); + expect(resources).toEqual(['calls', 'participants']); + expect(resources).not.toContain('add'); + }); + + test('Slack-style paths', () => { + const components = [ + { type: 'RESOURCE' as const, value: 'chat' }, + { type: 'RESOURCE' as const, value: 'postMessage' } + ]; + expect(engine.extractResourceChain(components, [])).toEqual(['chat', 'Message']); + }); +}); + +describe('determineVerb', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('GET collection returns list', () => { + expect(engine.determineVerb('GET', true)).toBe('list'); + }); + + test('GET item returns get', () => { + expect(engine.determineVerb('GET', false)).toBe('get'); + }); + + test('POST returns create', () => { + expect(engine.determineVerb('POST', true)).toBe('create'); + expect(engine.determineVerb('POST', false)).toBe('create'); + }); + + test('PUT/PATCH return update', () => { + expect(engine.determineVerb('PUT', false)).toBe('update'); + expect(engine.determineVerb('PATCH', false)).toBe('update'); + }); + + test('DELETE returns delete', () => { + expect(engine.determineVerb('DELETE', false)).toBe('delete'); + }); +}); + +describe('applySingularization', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('singularizes non-final resources', () => { + expect(engine.applySingularization(['repos', 'issues'], 'get')).toEqual(['repo', 'issue']); + }); + + test('pluralizes final resource for list', () => { + expect(engine.applySingularization(['repo', 'issue'], 'list')).toEqual(['repo', 'issues']); + }); + + test('singularizes final resource for item operations', () => { + expect(engine.applySingularization(['repos', 'issues'], 'get')).toEqual(['repo', 'issue']); + expect(engine.applySingularization(['repos', 'issues'], 'create')).toEqual(['repo', 'issue']); + expect(engine.applySingularization(['repos', 'issues'], 'update')).toEqual(['repo', 'issue']); + }); + + test('uncountable words remain unchanged', () => { + expect(engine.applySingularization(['data'], 'list')).toEqual(['data']); + expect(engine.applySingularization(['data'], 'get')).toEqual(['data']); + }); +}); + +describe('applyAbbreviations', () => { + test('applies abbreviations', () => { + const engine = new ToolNamingEngine({ application: 'app', repository: 'repo' }); + expect(engine.applyAbbreviations(['application', 'repository'])).toEqual(['app', 'repo']); + }); + + test('case-insensitive lookup', () => { + const engine = new ToolNamingEngine({ application: 'app' }); + expect(engine.applyAbbreviations(['Application', 'APPLICATION'])).toEqual(['app', 'app']); + }); + + test('no abbreviation passthrough', () => { + const engine = new ToolNamingEngine({ application: 'app' }); + expect(engine.applyAbbreviations(['user', 'issue'])).toEqual(['user', 'issue']); + }); +}); + +describe('buildCamelCaseName', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('simple names', () => { + expect(engine.buildCamelCaseName('list', ['users'])).toBe('listUsers'); + expect(engine.buildCamelCaseName('get', ['user'])).toBe('getUser'); + }); + + test('nested resource names', () => { + expect(engine.buildCamelCaseName('list', ['repo', 'issues'])).toBe('listRepoIssues'); + expect(engine.buildCamelCaseName('create', ['gist', 'comment'])).toBe('createGistComment'); + }); + + test('kebab-case resources', () => { + expect(engine.buildCamelCaseName('list', ['scheduled-messages'])).toBe('listScheduledMessages'); + }); + + test('snake_case resources', () => { + expect(engine.buildCamelCaseName('list', ['balance_transactions'])).toBe('listBalanceTransactions'); + }); +}); + +describe('normalizeToCamelCase', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('kebab-case', () => { + expect(engine.normalizeToCamelCase('scheduled-messages')).toBe('ScheduledMessages'); + expect(engine.normalizeToCamelCase('balance-history')).toBe('BalanceHistory'); + }); + + test('snake_case', () => { + expect(engine.normalizeToCamelCase('balance_transactions')).toBe('BalanceTransactions'); + expect(engine.normalizeToCamelCase('user_profile')).toBe('UserProfile'); + }); + + test('PascalCase', () => { + expect(engine.normalizeToCamelCase('UserProfile')).toBe('UserProfile'); + expect(engine.normalizeToCamelCase('BalanceHistory')).toBe('BalanceHistory'); + }); + + test('already camelCase', () => { + expect(engine.normalizeToCamelCase('userProfile')).toBe('UserProfile'); + expect(engine.normalizeToCamelCase('balanceHistory')).toBe('BalanceHistory'); + }); + + test('with dots', () => { + expect(engine.normalizeToCamelCase('scheduled.messages')).toBe('ScheduledMessages'); + }); + + test('empty string', () => { + expect(engine.normalizeToCamelCase('')).toBe(''); + }); +}); + +describe('isValidToolName', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine({}); }); + + test('valid names', () => { + expect(engine.isValidToolName('listUsers')).toBe(true); + expect(engine.isValidToolName('getUser')).toBe(true); + expect(engine.isValidToolName('createRepoIssue')).toBe(true); + expect(engine.isValidToolName('a')).toBe(true); + }); + + test('empty name is invalid', () => { + expect(engine.isValidToolName('')).toBe(false); + }); + + test('names starting with numbers are invalid', () => { + expect(engine.isValidToolName('123user')).toBe(false); + expect(engine.isValidToolName('1list')).toBe(false); + }); + + test('names without alphanumeric chars are invalid', () => { + expect(engine.isValidToolName('___')).toBe(false); + expect(engine.isValidToolName('---')).toBe(false); + }); +}); + +describe('generateToolName', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine(); }); + + test('simple collection', () => { + expect(engine.generateToolName('/users', 'GET')).toBe('listUsers'); + }); + + test('simple item', () => { + expect(engine.generateToolName('/users/{id}', 'GET')).toBe('getUser'); + }); + + test('nested resources', () => { + expect(engine.generateToolName('/repos/{owner}/{repo}/issues', 'GET')).toBe('listRepoIssues'); + expect(engine.generateToolName('/repos/{owner}/{repo}/issues/{id}', 'GET')).toBe('getRepoIssue'); + }); + + test('with path prefix', () => { + expect(engine.generateToolName('/v1/balance', 'GET', '/v1')).toBe('listBalances'); + expect(engine.generateToolName('/rest/api/3/issue/{id}', 'DELETE', '/rest/api/3')).toBe('deleteIssue'); + }); + + test('POST method', () => { + expect(engine.generateToolName('/users', 'POST')).toBe('createUser'); + expect(engine.generateToolName('/repos/{owner}/{repo}/issues', 'POST')).toBe('createRepoIssue'); + }); + + test('DELETE method', () => { + expect(engine.generateToolName('/users/{id}', 'DELETE')).toBe('deleteUser'); + }); + + test('PUT/PATCH methods', () => { + expect(engine.generateToolName('/users/{id}', 'PUT')).toBe('updateUser'); + expect(engine.generateToolName('/users/{id}', 'PATCH')).toBe('updateUser'); + }); + + test('abbreviations applied', () => { + expect(engine.generateToolName('/applications', 'GET')).toBe('listApps'); + expect(engine.generateToolName('/repositories/{id}', 'GET')).toBe('getRepo'); + }); + + test('Slack dot notation', () => { + expect(engine.generateToolName('/chat.postMessage', 'POST')).toBe('createChatMsg'); + expect(engine.generateToolName('/conversations.history', 'GET')).toBe('listConversationHistory'); + }); + + test('file extensions', () => { + expect(engine.generateToolName('/Accounts.json', 'POST')).toBe('createAccount'); + }); + + test('container skipping', () => { + expect(engine.generateToolName('/repos/{owner}/{repo}/codespaces/devcontainers', 'GET')).toBe('listCodespaceDevcontainers'); + }); + + test('error: no path components', () => { + expect(() => engine.generateToolName('/', 'GET')).toThrow('no path components'); + }); + + test('error: no resources found', () => { + expect(() => engine.generateToolName('/add', 'POST')).toThrow('no resources found'); + }); +}); + +describe('real-world examples', () => { + let engine: ToolNamingEngine; + beforeEach(() => { engine = new ToolNamingEngine(); }); + + test('GitHub paths', () => { + expect(engine.generateToolName('/gists', 'GET')).toBe('listGists'); + expect(engine.generateToolName('/gists/{id}', 'GET')).toBe('getGist'); + expect(engine.generateToolName('/gists/{gist_id}/comments', 'GET')).toBe('listGistComments'); + expect(engine.generateToolName('/gists/{gist_id}/comments/{id}', 'DELETE')).toBe('deleteGistComment'); + }); + + test('Stripe paths', () => { + expect(engine.generateToolName('/v1/balance', 'GET', '/v1')).toBe('listBalances'); + expect(engine.generateToolName('/v1/charges/{id}/refunds', 'GET', '/v1')).toBe('listChargeRefunds'); + expect(engine.generateToolName('/v1/payment_intents', 'POST', '/v1')).toBe('createPaymentIntent'); + }); + + test('Twilio paths', () => { + const prefix = '/2010-04-01/Accounts/{AccountSid}'; + expect(engine.generateToolName(`${prefix}/Calls.json`, 'GET', prefix)).toBe('listCalls'); + expect(engine.generateToolName(`${prefix}/Messages.json`, 'POST', prefix)).toBe('createMsg'); + }); + + test('Jira paths', () => { + const prefix = '/rest/api/3'; + expect(engine.generateToolName(`${prefix}/dashboard`, 'GET', prefix)).toBe('listDashboards'); + expect(engine.generateToolName(`${prefix}/issue/{id}`, 'DELETE', prefix)).toBe('deleteIssue'); + }); + + test('Confluence paths', () => { + const prefix = '/wiki/api/v2'; + expect(engine.generateToolName(`${prefix}/attachments`, 'GET', prefix)).toBe('listAttachments'); + expect(engine.generateToolName(`${prefix}/blogposts`, 'POST', prefix)).toBe('createBlogpost'); + }); +}); From c5b67a31abfedb75d0e40a1acc90cd7de1b2f82d Mon Sep 17 00:00:00 2001 From: Nathan Allen Date: Fri, 13 Mar 2026 05:32:08 -0400 Subject: [PATCH 2/3] chore: update jest and types dependencies --- package-lock.json | 746 ++++++++++++++++++++++------------------------ package.json | 6 +- 2 files changed, 356 insertions(+), 396 deletions(-) diff --git a/package-lock.json b/package-lock.json index f876733..fd5a616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,11 @@ "jsonschema": "^1.5.0" }, "devDependencies": { - "@jest/globals": "^30.2.0", + "@jest/globals": "^30.3.0", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", - "@types/node": "^25.2.3", - "jest": "^30.2.0", + "@types/node": "^25.5.0", + "jest": "^30.3.0", "ts-jest": "^29.4.6", "typescript": "^5.9.3" }, @@ -522,21 +522,21 @@ "license": "MIT" }, "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "dev": true, "license": "MIT", "optional": true, @@ -545,9 +545,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "dev": true, "license": "MIT", "optional": true, @@ -625,17 +625,17 @@ } }, "node_modules/@jest/console": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", - "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", "slash": "^3.0.0" }, "engines": { @@ -643,39 +643,38 @@ } }, "node_modules/@jest/core": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", - "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", + "@jest/console": "30.3.0", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.2.0", - "jest-config": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-resolve-dependencies": "30.2.0", - "jest-runner": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "jest-watcher": "30.2.0", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", "slash": "^3.0.0" }, "engines": { @@ -691,9 +690,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", "dev": true, "license": "MIT", "engines": { @@ -701,39 +700,39 @@ } }, "node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-mock": "30.2.0" + "jest-mock": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.2.0", - "jest-snapshot": "30.2.0" + "expect": "30.3.0", + "jest-snapshot": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "dev": true, "license": "MIT", "dependencies": { @@ -744,18 +743,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -772,16 +771,16 @@ } }, "node_modules/@jest/globals": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", - "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/types": "30.2.0", - "jest-mock": "30.2.0" + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -802,32 +801,32 @@ } }, "node_modules/@jest/reporters": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", - "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", "collect-v8-coverage": "^1.0.2", "exit-x": "^0.2.2", - "glob": "^10.3.10", + "glob": "^10.5.0", "graceful-fs": "^4.2.11", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -858,13 +857,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", - "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -889,14 +888,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", - "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -905,15 +904,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", - "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", + "@jest/test-result": "30.3.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.3.0", "slash": "^3.0.0" }, "engines": { @@ -921,24 +920,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.3.0", "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", + "jest-util": "30.3.0", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" @@ -948,9 +946,9 @@ } }, "node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, "license": "MIT", "dependencies": { @@ -1071,9 +1069,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1182,13 +1180,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", - "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/stack-utils": { @@ -1557,16 +1555,16 @@ "license": "Python-2.0" }, "node_modules/babel-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", - "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.2.0", + "@jest/transform": "30.3.0", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.2.0", + "babel-preset-jest": "30.3.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -1599,9 +1597,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", - "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", "dev": true, "license": "MIT", "dependencies": { @@ -1639,13 +1637,13 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", - "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.2.0", + "babel-plugin-jest-hoist": "30.3.0", "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { @@ -1682,19 +1680,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -2014,9 +1999,9 @@ } }, "node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2168,18 +2153,18 @@ } }, "node_modules/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2202,19 +2187,6 @@ "bser": "2.1.1" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2465,16 +2437,6 @@ "node": ">=6" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2596,16 +2558,16 @@ } }, "node_modules/jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", - "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/types": "30.2.0", + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", "import-local": "^3.2.0", - "jest-cli": "30.2.0" + "jest-cli": "30.3.0" }, "bin": { "jest": "bin/jest.js" @@ -2623,14 +2585,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", - "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.2.0", + "jest-util": "30.3.0", "p-limit": "^3.1.0" }, "engines": { @@ -2638,29 +2600,29 @@ } }, "node_modules/jest-circus": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", - "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", "p-limit": "^3.1.0", - "pretty-format": "30.2.0", + "pretty-format": "30.3.0", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -2670,21 +2632,21 @@ } }, "node_modules/jest-cli": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", - "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", "yargs": "^17.7.2" }, "bin": { @@ -2703,34 +2665,33 @@ } }, "node_modules/jest-config": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", - "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.2.0", - "@jest/types": "30.2.0", - "babel-jest": "30.2.0", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", - "glob": "^10.3.10", + "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-circus": "30.2.0", + "jest-circus": "30.3.0", "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", + "jest-environment-node": "30.3.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-runner": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "micromatch": "^4.0.8", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", "parse-json": "^5.2.0", - "pretty-format": "30.2.0", + "pretty-format": "30.3.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -2755,16 +2716,16 @@ } }, "node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.0.1", + "@jest/diff-sequences": "30.3.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2784,57 +2745,57 @@ } }, "node_modules/jest-each": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", - "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "chalk": "^4.1.2", - "jest-util": "30.2.0", - "pretty-format": "30.2.0" + "jest-util": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", - "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-mock": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0" + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", "walker": "^1.0.8" }, "engines": { @@ -2844,50 +2805,63 @@ "fsevents": "^2.3.3" } }, + "node_modules/jest-haste-map/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jest-leak-detector": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", - "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -2895,16 +2869,29 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-util": "30.2.0" + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2939,18 +2926,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", - "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.3.0", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -2959,46 +2946,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", - "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.2.0" + "jest-snapshot": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", - "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/environment": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-leak-detector": "30.2.0", - "jest-message-util": "30.2.0", - "jest-resolve": "30.2.0", - "jest-runtime": "30.2.0", - "jest-util": "30.2.0", - "jest-watcher": "30.2.0", - "jest-worker": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -3007,32 +2994,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", - "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/globals": "30.2.0", + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", - "glob": "^10.3.10", + "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -3041,9 +3028,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", - "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3052,20 +3039,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.2.0", + "expect": "30.3.0", "graceful-fs": "^4.2.11", - "jest-diff": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "pretty-format": "30.2.0", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -3087,18 +3074,18 @@ } }, "node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3118,18 +3105,18 @@ } }, "node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3149,19 +3136,19 @@ } }, "node_modules/jest-watcher": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", - "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.2.0", + "jest-util": "30.3.0", "string-length": "^4.0.2" }, "engines": { @@ -3169,15 +3156,15 @@ } }, "node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", + "jest-util": "30.3.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -3362,20 +3349,6 @@ "dev": true, "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -3387,13 +3360,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3413,11 +3386,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -3701,9 +3674,9 @@ } }, "node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3984,13 +3957,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -4134,9 +4107,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4153,19 +4126,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/ts-jest": { "version": "29.4.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", @@ -4305,9 +4265,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 6250eb5..092c235 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,11 @@ "author": "OCP Contributors", "license": "MIT", "devDependencies": { - "@jest/globals": "^30.2.0", + "@jest/globals": "^30.3.0", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", - "@types/node": "^25.2.3", - "jest": "^30.2.0", + "@types/node": "^25.5.0", + "jest": "^30.3.0", "ts-jest": "^29.4.6", "typescript": "^5.9.3" }, From 38ba4dbbd8c1dce3a0ab725c967fc93f5d4e01f6 Mon Sep 17 00:00:00 2001 From: Nathan Allen Date: Sun, 15 Mar 2026 09:49:22 -0400 Subject: [PATCH 3/3] refactor: add debug logging; handle tool creation errors in parseOpenApiSpec --- package-lock.json | 21 +++++++++++++++++++-- package.json | 2 ++ src/parsers/openapi_parser.ts | 21 ++++++++++----------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd5a616..2e9dab3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "debug": "^4.4.0", "js-yaml": "^4.1.1", "jsonschema": "^1.5.0" }, "devDependencies": { "@jest/globals": "^30.3.0", + "@types/debug": "^4.1.12", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "@types/node": "^25.5.0", @@ -1134,6 +1136,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1179,6 +1191,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", @@ -1984,7 +2003,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3399,7 +3417,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/napi-postinstall": { diff --git a/package.json b/package.json index 092c235..7ae144f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "license": "MIT", "devDependencies": { "@jest/globals": "^30.3.0", + "@types/debug": "^4.1.12", "@types/jest": "^30.0.0", "@types/js-yaml": "^4.0.9", "@types/node": "^25.5.0", @@ -45,6 +46,7 @@ "node": ">=18.0.0" }, "dependencies": { + "debug": "^4.4.0", "js-yaml": "^4.1.1", "jsonschema": "^1.5.0" } diff --git a/src/parsers/openapi_parser.ts b/src/parsers/openapi_parser.ts index 71e71a8..3b81ab8 100644 --- a/src/parsers/openapi_parser.ts +++ b/src/parsers/openapi_parser.ts @@ -5,6 +5,9 @@ import { APISpecParser, OCPAPISpec, OCPTool } from './base.js'; import { SchemaDiscoveryError } from '../errors.js'; import { ToolNamingEngine } from './tool_naming.js'; +import debug from 'debug'; + +const log = debug('ocp:parser'); // Configuration constants const DEFAULT_API_TITLE = 'Unknown API'; @@ -111,9 +114,11 @@ export class OpenAPIParser extends APISpecParser { continue; } - const tool = this.createToolFromOperation(path, method.toUpperCase(), operation, spec, memoCache, pathPrefix); - if (tool) { + try { + const tool = this.createToolFromOperation(path, method.toUpperCase(), operation, spec, memoCache, pathPrefix); tools.push(tool); + } catch (e) { + log('Skipping %s %s: %s', method.toUpperCase(), path, e instanceof Error ? e.message : e); } } } @@ -162,19 +167,13 @@ export class OpenAPIParser extends APISpecParser { specData: Record, memoCache: Record, pathPrefix?: string - ): OCPTool | null { + ): OCPTool { if (!operation || typeof operation !== 'object') { - return null; + throw new Error(`Invalid operation object for ${method} ${path}`); } // Generate tool name from path and method using the naming engine - let toolName: string; - try { - toolName = this.namingEngine.generateToolName(path, method, pathPrefix); - } catch { - console.warn(`Skipping operation ${method} ${path}: unable to generate valid tool name`); - return null; - } + const toolName = this.namingEngine.generateToolName(path, method, pathPrefix); // Store operation ID for reference const operationId = operation.operationId || undefined;