mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
* feat: add structural hashes and success tracking for workflow mutations Enables cross-referencing workflow_mutations with telemetry_workflows by adding structural hashes (nodeTypes + connections) alongside existing full hashes. **Database Changes:** - Added workflow_structure_hash_before/after columns - Added is_truly_successful computed column - Created 3 analytics views: successful_mutations, mutation_training_data, mutations_with_workflow_quality - Created 2 helper functions: get_mutation_success_rate_by_intent(), get_mutation_crossref_stats() **Code Changes:** - Updated mutation-tracker.ts to generate both hash types - Updated mutation-types.ts with new fields - Auto-converts to snake_case via existing toSnakeCase() function **Testing:** - Added 5 new unit tests for structural hash generation - All 17 tests passing **Tooling:** - Created backfill script to populate hashes for existing 1,499 mutations - Created comprehensive documentation (STRUCTURAL_HASHES.md) **Impact:** - Before: 0% cross-reference match rate - After: Expected 60-70% match rate (post-backfill) - Unlocks quality impact analysis, training data curation, and mutation pattern insights Conceived by Romuald Członkowski - www.aiadvisors.pl/en * fix: correct test operation types for structural hash tests Fixed TypeScript errors in mutation-tracker tests by adding required 'updates' parameter to updateNode operations. Used 'as any' for test operations to maintain backward compatibility while tests are updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en * chore: remove documentation files from tracking Removed internal documentation files from version control: - Telemetry implementation docs - Implementation roadmap - Disabled tools analysis docs These files are for internal reference only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en * chore: remove telemetry documentation files from tracking Removed all telemetry analysis and documentation files from root directory. These files are for internal reference only and should not be in version control. Files removed: - TELEMETRY_ANALYSIS*.md - TELEMETRY_MUTATION_SPEC.md - TELEMETRY_*_DATASET.md - VALIDATION_ANALYSIS*.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en * chore: bump version to 2.22.18 and update CHANGELOG Version 2.22.18 adds structural hash tracking for workflow mutations, enabling cross-referencing with workflow quality data and automated success detection. Key changes: - Added workflowStructureHashBefore/After fields - Added isTrulySuccessful computed field - Enhanced mutation tracking with structural hashes - All tests passing (17/17) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en * chore: remove migration and documentation files from PR Removed internal database migration files and documentation from version control: - docs/migrations/ - docs/telemetry/ Updated CHANGELOG to remove database migration references. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
284 lines
8.3 KiB
TypeScript
284 lines
8.3 KiB
TypeScript
/**
|
|
* Core mutation tracker for workflow transformations
|
|
* Coordinates validation, classification, and metric calculation
|
|
*/
|
|
|
|
import { DiffOperation } from '../types/workflow-diff.js';
|
|
import {
|
|
WorkflowMutationData,
|
|
WorkflowMutationRecord,
|
|
MutationChangeMetrics,
|
|
MutationValidationMetrics,
|
|
IntentClassification,
|
|
} from './mutation-types.js';
|
|
import { intentClassifier } from './intent-classifier.js';
|
|
import { mutationValidator } from './mutation-validator.js';
|
|
import { intentSanitizer } from './intent-sanitizer.js';
|
|
import { WorkflowSanitizer } from './workflow-sanitizer.js';
|
|
import { logger } from '../utils/logger.js';
|
|
|
|
/**
|
|
* Tracks workflow mutations and prepares data for telemetry
|
|
*/
|
|
export class MutationTracker {
|
|
private recentMutations: Array<{
|
|
hashBefore: string;
|
|
hashAfter: string;
|
|
operations: DiffOperation[];
|
|
}> = [];
|
|
|
|
private readonly RECENT_MUTATIONS_LIMIT = 100;
|
|
|
|
/**
|
|
* Process and prepare mutation data for tracking
|
|
*/
|
|
async processMutation(data: WorkflowMutationData, userId: string): Promise<WorkflowMutationRecord | null> {
|
|
try {
|
|
// Validate data quality
|
|
if (!this.validateMutationData(data)) {
|
|
logger.debug('Mutation data validation failed');
|
|
return null;
|
|
}
|
|
|
|
// Sanitize workflows to remove credentials and sensitive data
|
|
const workflowBefore = WorkflowSanitizer.sanitizeWorkflowRaw(data.workflowBefore);
|
|
const workflowAfter = WorkflowSanitizer.sanitizeWorkflowRaw(data.workflowAfter);
|
|
|
|
// Sanitize user intent
|
|
const sanitizedIntent = intentSanitizer.sanitize(data.userIntent);
|
|
|
|
// Check if should be excluded
|
|
if (mutationValidator.shouldExclude(data)) {
|
|
logger.debug('Mutation excluded from tracking based on quality criteria');
|
|
return null;
|
|
}
|
|
|
|
// Check for duplicates
|
|
if (
|
|
mutationValidator.isDuplicate(
|
|
workflowBefore,
|
|
workflowAfter,
|
|
data.operations,
|
|
this.recentMutations
|
|
)
|
|
) {
|
|
logger.debug('Duplicate mutation detected, skipping tracking');
|
|
return null;
|
|
}
|
|
|
|
// Generate hashes
|
|
const hashBefore = mutationValidator.hashWorkflow(workflowBefore);
|
|
const hashAfter = mutationValidator.hashWorkflow(workflowAfter);
|
|
|
|
// Generate structural hashes for cross-referencing with telemetry_workflows
|
|
const structureHashBefore = WorkflowSanitizer.generateWorkflowHash(workflowBefore);
|
|
const structureHashAfter = WorkflowSanitizer.generateWorkflowHash(workflowAfter);
|
|
|
|
// Classify intent
|
|
const intentClassification = intentClassifier.classify(data.operations, sanitizedIntent);
|
|
|
|
// Calculate metrics
|
|
const changeMetrics = this.calculateChangeMetrics(data.operations);
|
|
const validationMetrics = this.calculateValidationMetrics(
|
|
data.validationBefore,
|
|
data.validationAfter
|
|
);
|
|
|
|
// Create mutation record
|
|
const record: WorkflowMutationRecord = {
|
|
userId,
|
|
sessionId: data.sessionId,
|
|
workflowBefore,
|
|
workflowAfter,
|
|
workflowHashBefore: hashBefore,
|
|
workflowHashAfter: hashAfter,
|
|
workflowStructureHashBefore: structureHashBefore,
|
|
workflowStructureHashAfter: structureHashAfter,
|
|
userIntent: sanitizedIntent,
|
|
intentClassification,
|
|
toolName: data.toolName,
|
|
operations: data.operations,
|
|
operationCount: data.operations.length,
|
|
operationTypes: this.extractOperationTypes(data.operations),
|
|
validationBefore: data.validationBefore,
|
|
validationAfter: data.validationAfter,
|
|
...validationMetrics,
|
|
...changeMetrics,
|
|
mutationSuccess: data.mutationSuccess,
|
|
mutationError: data.mutationError,
|
|
durationMs: data.durationMs,
|
|
};
|
|
|
|
// Store in recent mutations for deduplication
|
|
this.addToRecentMutations(hashBefore, hashAfter, data.operations);
|
|
|
|
return record;
|
|
} catch (error) {
|
|
logger.error('Error processing mutation:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate mutation data
|
|
*/
|
|
private validateMutationData(data: WorkflowMutationData): boolean {
|
|
const validationResult = mutationValidator.validate(data);
|
|
|
|
if (!validationResult.valid) {
|
|
logger.warn('Mutation data validation failed:', validationResult.errors);
|
|
return false;
|
|
}
|
|
|
|
if (validationResult.warnings.length > 0) {
|
|
logger.debug('Mutation data validation warnings:', validationResult.warnings);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Calculate change metrics from operations
|
|
*/
|
|
private calculateChangeMetrics(operations: DiffOperation[]): MutationChangeMetrics {
|
|
const metrics: MutationChangeMetrics = {
|
|
nodesAdded: 0,
|
|
nodesRemoved: 0,
|
|
nodesModified: 0,
|
|
connectionsAdded: 0,
|
|
connectionsRemoved: 0,
|
|
propertiesChanged: 0,
|
|
};
|
|
|
|
for (const op of operations) {
|
|
switch (op.type) {
|
|
case 'addNode':
|
|
metrics.nodesAdded++;
|
|
break;
|
|
case 'removeNode':
|
|
metrics.nodesRemoved++;
|
|
break;
|
|
case 'updateNode':
|
|
metrics.nodesModified++;
|
|
if ('updates' in op && op.updates) {
|
|
metrics.propertiesChanged += Object.keys(op.updates as any).length;
|
|
}
|
|
break;
|
|
case 'addConnection':
|
|
metrics.connectionsAdded++;
|
|
break;
|
|
case 'removeConnection':
|
|
metrics.connectionsRemoved++;
|
|
break;
|
|
case 'rewireConnection':
|
|
// Rewiring is effectively removing + adding
|
|
metrics.connectionsRemoved++;
|
|
metrics.connectionsAdded++;
|
|
break;
|
|
case 'replaceConnections':
|
|
// Count how many connections are being replaced
|
|
if ('connections' in op && op.connections) {
|
|
metrics.connectionsRemoved++;
|
|
metrics.connectionsAdded++;
|
|
}
|
|
break;
|
|
case 'updateSettings':
|
|
if ('settings' in op && op.settings) {
|
|
metrics.propertiesChanged += Object.keys(op.settings as any).length;
|
|
}
|
|
break;
|
|
case 'moveNode':
|
|
case 'enableNode':
|
|
case 'disableNode':
|
|
case 'updateName':
|
|
case 'addTag':
|
|
case 'removeTag':
|
|
case 'activateWorkflow':
|
|
case 'deactivateWorkflow':
|
|
case 'cleanStaleConnections':
|
|
// These don't directly affect node/connection counts
|
|
// but count as property changes
|
|
metrics.propertiesChanged++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return metrics;
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculate validation improvement metrics
|
|
*/
|
|
private calculateValidationMetrics(
|
|
validationBefore: any,
|
|
validationAfter: any
|
|
): MutationValidationMetrics {
|
|
// If validation data is missing, return nulls
|
|
if (!validationBefore || !validationAfter) {
|
|
return {
|
|
validationImproved: null,
|
|
errorsResolved: 0,
|
|
errorsIntroduced: 0,
|
|
};
|
|
}
|
|
|
|
const errorsBefore = validationBefore.errors?.length || 0;
|
|
const errorsAfter = validationAfter.errors?.length || 0;
|
|
|
|
const errorsResolved = Math.max(0, errorsBefore - errorsAfter);
|
|
const errorsIntroduced = Math.max(0, errorsAfter - errorsBefore);
|
|
|
|
const validationImproved = errorsBefore > errorsAfter;
|
|
|
|
return {
|
|
validationImproved,
|
|
errorsResolved,
|
|
errorsIntroduced,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract unique operation types from operations
|
|
*/
|
|
private extractOperationTypes(operations: DiffOperation[]): string[] {
|
|
const types = new Set(operations.map((op) => op.type));
|
|
return Array.from(types);
|
|
}
|
|
|
|
/**
|
|
* Add mutation to recent list for deduplication
|
|
*/
|
|
private addToRecentMutations(
|
|
hashBefore: string,
|
|
hashAfter: string,
|
|
operations: DiffOperation[]
|
|
): void {
|
|
this.recentMutations.push({ hashBefore, hashAfter, operations });
|
|
|
|
// Keep only recent mutations
|
|
if (this.recentMutations.length > this.RECENT_MUTATIONS_LIMIT) {
|
|
this.recentMutations.shift();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear recent mutations (useful for testing)
|
|
*/
|
|
clearRecentMutations(): void {
|
|
this.recentMutations = [];
|
|
}
|
|
|
|
/**
|
|
* Get statistics about tracked mutations
|
|
*/
|
|
getRecentMutationsCount(): number {
|
|
return this.recentMutations.length;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Singleton instance for easy access
|
|
*/
|
|
export const mutationTracker = new MutationTracker();
|