Skip to main content
Glama

Poker Task Management MCP

by Hirao-Y
TransformValidator.js16.4 kB
// validators/TransformValidator.js import { PokerMcpError } from '../utils/mcpErrors.js'; import { ManifestValidator } from './ManifestValidator.js'; /** * Transform参照の検証を行うクラス(強化版) * YAMLデータのtransformセクション内での名前存在確認と依存関係管理 */ export class TransformValidator { /** * Transform参照の存在確認 * @param {string} transformName - 参照するtransform名 * @param {Array} existingTransforms - YAMLファイル内のtransform配列 * @param {string} fieldName - フィールド名(エラー表示用) */ static validateTransformReference(transformName, existingTransforms, fieldName = 'transform') { // transform名自体のバリデーション ManifestValidator.validateObjectName(transformName, fieldName); // 既存transformsがnullまたは未定義の場合 if (!existingTransforms || !Array.isArray(existingTransforms)) { throw PokerMcpError.transformNotFound(transformName); } // transform名の存在確認 const transformExists = existingTransforms.some(t => t && t.name === transformName); if (!transformExists) { throw PokerMcpError.invalidTransformReference(transformName); } return true; } /** * Transform操作の基本バリデーション * @param {Array} operations - 変換操作の配列 */ static validateTransformOperations(operations) { if (!Array.isArray(operations) || operations.length === 0) { throw PokerMcpError.validationError( 'operations must be a non-empty array', 'operations', operations ); } for (let i = 0; i < operations.length; i++) { const operation = operations[i]; if (!operation || typeof operation !== 'object') { throw PokerMcpError.validationError( `operation[${i}] must be an object`, `operations[${i}]`, operation ); } const keys = Object.keys(operation); if (keys.length !== 1) { throw PokerMcpError.validationError( `operation[${i}] must have exactly one key`, `operations[${i}]`, operation ); } const operationType = keys[0]; const operationValue = operation[operationType]; switch (operationType) { case 'translate': ManifestValidator.validateCoordinateString(operationValue, `operations[${i}].translate`); break; case 'rotate_around_x': case 'rotate_around_y': case 'rotate_around_z': ManifestValidator.validateRotationAngle(operationValue, `operations[${i}].${operationType}`); break; default: throw PokerMcpError.validationError( `Invalid operation type: ${operationType}. Valid types: translate, rotate_around_x, rotate_around_y, rotate_around_z`, `operations[${i}]`, operationType ); } } return true; } /** * Transform操作の物理的妥当性チェック * @param {Array} operations - 変換操作の配列 * @param {string} transformName - Transform名(エラー表示用) */ static validateTransformPhysics(operations, transformName = 'transform') { let hasRotation = false; let hasTranslation = false; let totalRotationMagnitude = 0; for (let i = 0; i < operations.length; i++) { const operation = operations[i]; const operationType = Object.keys(operation)[0]; const operationValue = operation[operationType]; switch (operationType) { case 'translate': hasTranslation = true; // 移動量の大きさチェック const [dx, dy, dz] = operationValue.split(/\s+/).map(Number); const translationMagnitude = Math.sqrt(dx*dx + dy*dy + dz*dz); if (translationMagnitude > 10000) { console.warn(`Transform '${transformName}': 非常に大きな移動量です (${translationMagnitude.toFixed(2)})`); } break; case 'rotate_around_x': case 'rotate_around_y': case 'rotate_around_z': hasRotation = true; totalRotationMagnitude += Math.abs(operationValue); // 異常な回転角度のチェック if (Math.abs(operationValue) > 360) { console.warn(`Transform '${transformName}': 360度を超える回転角度です (${operationValue}度)`); } break; } } // 総回転量のチェック if (totalRotationMagnitude > 720) { console.warn(`Transform '${transformName}': 総回転量が非常に大きいです (${totalRotationMagnitude.toFixed(1)}度)`); } // Transformの意味チェック if (!hasRotation && !hasTranslation) { console.warn(`Transform '${transformName}': 回転も移動も含まれていません`); } return { hasRotation, hasTranslation, totalRotationMagnitude, operationCount: operations.length }; } /** * Transform名の一意性チェック * @param {string} transformName - 新しいTransform名 * @param {Array} existingTransforms - 既存Transform配列 * @param {Array} pendingChanges - 保留中の変更配列 */ static validateTransformUniqueness(transformName, existingTransforms = [], pendingChanges = []) { // 既存Transformとの重複チェック if (existingTransforms.some(t => t && t.name === transformName)) { throw PokerMcpError.duplicateName(transformName, 'transform'); } // 保留中の変更との重複チェック const pendingTransformChanges = pendingChanges.filter( change => change.action === 'proposeTransform' ); for (const change of pendingTransformChanges) { if (change.data && change.data.name === transformName) { throw PokerMcpError.duplicateName(transformName, 'transform (pending)'); } } return true; } /** * Body/Source/DetectorのTransform参照を検証 * @param {string} transformName - 参照するtransform名 * @param {Object} yamlData - YAMLデータ全体 * @param {string} contextType - コンテキストタイプ(body/source/detector) * @param {string} contextName - コンテキスト名 */ static validateContextTransformReference(transformName, yamlData, contextType, contextName) { if (!transformName) { return true; // transform参照はオプション } try { this.validateTransformReference(transformName, yamlData.transform, 'transform'); } catch (error) { // エラーメッセージにコンテキスト情報を追加 if (error instanceof PokerMcpError) { throw new PokerMcpError( error.code, `${contextType} '${contextName}' references invalid transform: ${error.message}`, 'transform', transformName ); } throw error; } return true; } /** * Transform依存関係チェック(詳細版) * Transform削除時に、そのTransformを参照しているBody/Source/Detectorがないかチェック * @param {string} transformName - 削除対象のtransform名 * @param {Object} yamlData - YAMLデータ全体 * @param {Array} pendingChanges - 保留中の変更配列 */ static checkTransformDependencies(transformName, yamlData, pendingChanges = []) { const dependencies = []; // 既存データの依存関係チェック this._checkExistingDependencies(transformName, yamlData, dependencies); // 保留中の変更の依存関係チェック this._checkPendingDependencies(transformName, pendingChanges, dependencies); if (dependencies.length > 0) { throw PokerMcpError.dependencyExists(transformName, dependencies); } return { dependencies: [], canDelete: true, checkedSections: ['body', 'source', 'detector', 'pending_changes'] }; } /** * 既存データの依存関係チェック * @private */ static _checkExistingDependencies(transformName, yamlData, dependencies) { // Body内のtransform参照をチェック if (yamlData.body && Array.isArray(yamlData.body)) { for (const body of yamlData.body) { if (body.transform === transformName) { dependencies.push({ type: 'body', name: body.name, context: `Body '${body.name}' uses transform '${transformName}'` }); } } } // Source内のtransform参照をチェック if (yamlData.source && Array.isArray(yamlData.source)) { for (const source of yamlData.source) { // geometry内のtransform参照をチェック if (source.geometry && source.geometry.transform === transformName) { dependencies.push({ type: 'source', name: source.name, context: `Source '${source.name}' geometry uses transform '${transformName}'` }); } } } // Detector内のtransform参照をチェック if (yamlData.detector && Array.isArray(yamlData.detector)) { for (const detector of yamlData.detector) { if (detector.transform === transformName) { dependencies.push({ type: 'detector', name: detector.name, context: `Detector '${detector.name}' uses transform '${transformName}'` }); } } } } /** * 保留中の変更の依存関係チェック * @private */ static _checkPendingDependencies(transformName, pendingChanges, dependencies) { for (const change of pendingChanges) { // Body関連の保留中変更 if (change.action === 'add_body' || change.action === 'updateBody') { const bodyData = change.data.body || change.data; if (bodyData && bodyData.transform === transformName) { dependencies.push({ type: 'pending_body', name: bodyData.name, context: `Pending body '${bodyData.name}' will use transform '${transformName}'` }); } } // Source関連の保留中変更 if (change.action === 'proposeSource' || change.action === 'updateSource') { const sourceData = change.data; if (sourceData && sourceData.geometry && sourceData.geometry.transform === transformName) { dependencies.push({ type: 'pending_source', name: sourceData.name, context: `Pending source '${sourceData.name}' will use transform '${transformName}'` }); } } // Detector関連の保留中変更 if (change.action === 'proposeDetector' || change.action === 'updateDetector') { const detectorData = change.data; if (detectorData && detectorData.transform === transformName) { dependencies.push({ type: 'pending_detector', name: detectorData.name, context: `Pending detector '${detectorData.name}' will use transform '${transformName}'` }); } } } } /** * Transform参照の完全性チェック * システム全体で参照されているtransformが実際に存在するかチェック * @param {Object} yamlData - YAMLデータ全体 */ static validateTransformIntegrity(yamlData) { const issues = []; const availableTransforms = new Set(); // 利用可能なTransform名を収集 if (yamlData.transform && Array.isArray(yamlData.transform)) { for (const transform of yamlData.transform) { if (transform && transform.name) { availableTransforms.add(transform.name); } } } // Bodyのtransform参照をチェック if (yamlData.body && Array.isArray(yamlData.body)) { for (const body of yamlData.body) { if (body.transform && !availableTransforms.has(body.transform)) { issues.push({ type: 'missing_transform', context: 'body', name: body.name, missingTransform: body.transform, message: `Body '${body.name}' references non-existent transform '${body.transform}'` }); } } } // Sourceのtransform参照をチェック if (yamlData.source && Array.isArray(yamlData.source)) { for (const source of yamlData.source) { if (source.geometry && source.geometry.transform && !availableTransforms.has(source.geometry.transform)) { issues.push({ type: 'missing_transform', context: 'source', name: source.name, missingTransform: source.geometry.transform, message: `Source '${source.name}' references non-existent transform '${source.geometry.transform}'` }); } } } // Detectorのtransform参照をチェック if (yamlData.detector && Array.isArray(yamlData.detector)) { for (const detector of yamlData.detector) { if (detector.transform && !availableTransforms.has(detector.transform)) { issues.push({ type: 'missing_transform', context: 'detector', name: detector.name, missingTransform: detector.transform, message: `Detector '${detector.name}' references non-existent transform '${detector.transform}'` }); } } } return { isValid: issues.length === 0, issues, availableTransforms: Array.from(availableTransforms), checkedReferences: { body: yamlData.body?.length || 0, source: yamlData.source?.length || 0, detector: yamlData.detector?.length || 0 } }; } /** * Transform使用状況の統計情報を取得 * @param {Object} yamlData - YAMLデータ全体 */ static getTransformUsageStats(yamlData) { const stats = new Map(); const allTransforms = new Set(); // 定義されているTransformを登録 if (yamlData.transform && Array.isArray(yamlData.transform)) { for (const transform of yamlData.transform) { if (transform && transform.name) { allTransforms.add(transform.name); stats.set(transform.name, { name: transform.name, operationCount: transform.operations?.length || 0, usedBy: { body: [], source: [], detector: [] }, totalUsage: 0 }); } } } // Bodyでの使用状況を記録 if (yamlData.body && Array.isArray(yamlData.body)) { for (const body of yamlData.body) { if (body.transform && stats.has(body.transform)) { const stat = stats.get(body.transform); stat.usedBy.body.push(body.name); stat.totalUsage++; } } } // Sourceでの使用状況を記録 if (yamlData.source && Array.isArray(yamlData.source)) { for (const source of yamlData.source) { if (source.geometry && source.geometry.transform && stats.has(source.geometry.transform)) { const stat = stats.get(source.geometry.transform); stat.usedBy.source.push(source.name); stat.totalUsage++; } } } // Detectorでの使用状況を記録 if (yamlData.detector && Array.isArray(yamlData.detector)) { for (const detector of yamlData.detector) { if (detector.transform && stats.has(detector.transform)) { const stat = stats.get(detector.transform); stat.usedBy.detector.push(detector.name); stat.totalUsage++; } } } return { totalTransforms: allTransforms.size, usedTransforms: Array.from(stats.values()).filter(s => s.totalUsage > 0).length, unusedTransforms: Array.from(stats.values()).filter(s => s.totalUsage === 0).map(s => s.name), detailedStats: Array.from(stats.values()), summary: { mostUsed: Array.from(stats.values()).sort((a, b) => b.totalUsage - a.totalUsage)[0], averageUsage: Array.from(stats.values()).reduce((sum, s) => sum + s.totalUsage, 0) / allTransforms.size || 0 } }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Hirao-Y/poker_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server