mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-20 17:33:08 +00:00
- Add universal expression validator with 100% reliable detection - Implement confidence-based scoring for node-specific recommendations - Add resource locator format detection and validation - Fix pattern matching precision (exact/prefix instead of includes) - Add recursion depth protection (MAX_RECURSION_DEPTH = 100) - Validate resource locator modes (id, url, expression, name, list) - Separate universal rules from node-specific intelligence - Add comprehensive test coverage (94%+ statements) - Prevent common AI agent mistakes with expressions Addresses code review feedback with critical fixes and enhancements. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
211 lines
6.7 KiB
TypeScript
211 lines
6.7 KiB
TypeScript
/**
|
|
* Confidence Scorer for node-specific validations
|
|
*
|
|
* Provides confidence scores for node-specific recommendations,
|
|
* allowing users to understand the reliability of suggestions.
|
|
*/
|
|
|
|
export interface ConfidenceScore {
|
|
value: number; // 0.0 to 1.0
|
|
reason: string;
|
|
factors: ConfidenceFactor[];
|
|
}
|
|
|
|
export interface ConfidenceFactor {
|
|
name: string;
|
|
weight: number;
|
|
matched: boolean;
|
|
description: string;
|
|
}
|
|
|
|
export class ConfidenceScorer {
|
|
/**
|
|
* Calculate confidence score for resource locator recommendation
|
|
*/
|
|
static scoreResourceLocatorRecommendation(
|
|
fieldName: string,
|
|
nodeType: string,
|
|
value: string
|
|
): ConfidenceScore {
|
|
const factors: ConfidenceFactor[] = [];
|
|
let totalWeight = 0;
|
|
let matchedWeight = 0;
|
|
|
|
// Factor 1: Exact field name match (highest confidence)
|
|
const exactFieldMatch = this.checkExactFieldMatch(fieldName, nodeType);
|
|
factors.push({
|
|
name: 'exact-field-match',
|
|
weight: 0.5,
|
|
matched: exactFieldMatch,
|
|
description: `Field name '${fieldName}' is known to use resource locator in ${nodeType}`
|
|
});
|
|
|
|
// Factor 2: Field name pattern (medium confidence)
|
|
const patternMatch = this.checkFieldPattern(fieldName);
|
|
factors.push({
|
|
name: 'field-pattern',
|
|
weight: 0.3,
|
|
matched: patternMatch,
|
|
description: `Field name '${fieldName}' matches common resource locator patterns`
|
|
});
|
|
|
|
// Factor 3: Value pattern (low confidence)
|
|
const valuePattern = this.checkValuePattern(value);
|
|
factors.push({
|
|
name: 'value-pattern',
|
|
weight: 0.1,
|
|
matched: valuePattern,
|
|
description: 'Value contains patterns typical of resource identifiers'
|
|
});
|
|
|
|
// Factor 4: Node type category (medium confidence)
|
|
const nodeCategory = this.checkNodeCategory(nodeType);
|
|
factors.push({
|
|
name: 'node-category',
|
|
weight: 0.1,
|
|
matched: nodeCategory,
|
|
description: `Node type '${nodeType}' typically uses resource locators`
|
|
});
|
|
|
|
// Calculate final score
|
|
for (const factor of factors) {
|
|
totalWeight += factor.weight;
|
|
if (factor.matched) {
|
|
matchedWeight += factor.weight;
|
|
}
|
|
}
|
|
|
|
const score = totalWeight > 0 ? matchedWeight / totalWeight : 0;
|
|
|
|
// Determine reason based on score
|
|
let reason: string;
|
|
if (score >= 0.8) {
|
|
reason = 'High confidence: Multiple strong indicators suggest resource locator format';
|
|
} else if (score >= 0.5) {
|
|
reason = 'Medium confidence: Some indicators suggest resource locator format';
|
|
} else if (score >= 0.3) {
|
|
reason = 'Low confidence: Weak indicators for resource locator format';
|
|
} else {
|
|
reason = 'Very low confidence: Minimal evidence for resource locator format';
|
|
}
|
|
|
|
return {
|
|
value: score,
|
|
reason,
|
|
factors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Known field mappings with exact matches
|
|
*/
|
|
private static readonly EXACT_FIELD_MAPPINGS: Record<string, string[]> = {
|
|
'github': ['owner', 'repository', 'user', 'organization'],
|
|
'googlesheets': ['sheetId', 'documentId', 'spreadsheetId'],
|
|
'googledrive': ['fileId', 'folderId', 'driveId'],
|
|
'slack': ['channel', 'user', 'channelId', 'userId'],
|
|
'notion': ['databaseId', 'pageId', 'blockId'],
|
|
'airtable': ['baseId', 'tableId', 'viewId']
|
|
};
|
|
|
|
private static checkExactFieldMatch(fieldName: string, nodeType: string): boolean {
|
|
const nodeBase = nodeType.split('.').pop()?.toLowerCase() || '';
|
|
|
|
for (const [pattern, fields] of Object.entries(this.EXACT_FIELD_MAPPINGS)) {
|
|
if (nodeBase === pattern || nodeBase.startsWith(`${pattern}-`)) {
|
|
return fields.includes(fieldName);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Common patterns in field names that suggest resource locators
|
|
*/
|
|
private static readonly FIELD_PATTERNS = [
|
|
/^.*Id$/i, // ends with Id
|
|
/^.*Ids$/i, // ends with Ids
|
|
/^.*Key$/i, // ends with Key
|
|
/^.*Name$/i, // ends with Name
|
|
/^.*Path$/i, // ends with Path
|
|
/^.*Url$/i, // ends with Url
|
|
/^.*Uri$/i, // ends with Uri
|
|
/^(table|database|collection|bucket|folder|file|document|sheet|board|project|issue|user|channel|team|organization|repository|owner)$/i
|
|
];
|
|
|
|
private static checkFieldPattern(fieldName: string): boolean {
|
|
return this.FIELD_PATTERNS.some(pattern => pattern.test(fieldName));
|
|
}
|
|
|
|
/**
|
|
* Check if the value looks like it contains identifiers
|
|
*/
|
|
private static checkValuePattern(value: string): boolean {
|
|
// Remove = prefix if present for analysis
|
|
const content = value.startsWith('=') ? value.substring(1) : value;
|
|
|
|
// Skip if not an expression
|
|
if (!content.includes('{{') || !content.includes('}}')) {
|
|
return false;
|
|
}
|
|
|
|
// Check for patterns that suggest IDs or resource references
|
|
const patterns = [
|
|
/\{\{.*\.(id|Id|ID|key|Key|name|Name|path|Path|url|Url|uri|Uri).*\}\}/i,
|
|
/\{\{.*_(id|Id|ID|key|Key|name|Name|path|Path|url|Url|uri|Uri).*\}\}/i,
|
|
/\{\{.*(id|Id|ID|key|Key|name|Name|path|Path|url|Url|uri|Uri).*\}\}/i
|
|
];
|
|
|
|
return patterns.some(pattern => pattern.test(content));
|
|
}
|
|
|
|
/**
|
|
* Node categories that commonly use resource locators
|
|
*/
|
|
private static readonly RESOURCE_HEAVY_NODES = [
|
|
'github', 'gitlab', 'bitbucket', // Version control
|
|
'googlesheets', 'googledrive', 'dropbox', // Cloud storage
|
|
'slack', 'discord', 'telegram', // Communication
|
|
'notion', 'airtable', 'baserow', // Databases
|
|
'jira', 'asana', 'trello', 'monday', // Project management
|
|
'salesforce', 'hubspot', 'pipedrive', // CRM
|
|
'stripe', 'paypal', 'square', // Payment
|
|
'aws', 'gcp', 'azure', // Cloud providers
|
|
'mysql', 'postgres', 'mongodb', 'redis' // Databases
|
|
];
|
|
|
|
private static checkNodeCategory(nodeType: string): boolean {
|
|
const nodeBase = nodeType.split('.').pop()?.toLowerCase() || '';
|
|
|
|
return this.RESOURCE_HEAVY_NODES.some(category =>
|
|
nodeBase.includes(category)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get confidence level as a string
|
|
*/
|
|
static getConfidenceLevel(score: number): 'high' | 'medium' | 'low' | 'very-low' {
|
|
if (score >= 0.8) return 'high';
|
|
if (score >= 0.5) return 'medium';
|
|
if (score >= 0.3) return 'low';
|
|
return 'very-low';
|
|
}
|
|
|
|
/**
|
|
* Should apply recommendation based on confidence and threshold
|
|
*/
|
|
static shouldApplyRecommendation(
|
|
score: number,
|
|
threshold: 'strict' | 'normal' | 'relaxed' = 'normal'
|
|
): boolean {
|
|
const thresholds = {
|
|
strict: 0.8, // Only apply high confidence recommendations
|
|
normal: 0.5, // Apply medium and high confidence
|
|
relaxed: 0.3 // Apply low, medium, and high confidence
|
|
};
|
|
|
|
return score >= thresholds[threshold];
|
|
}
|
|
} |