From c7f8614de1c09e58f9cd7d56ddbf853fb4d4dad8 Mon Sep 17 00:00:00 2001 From: czlonkowski <56956555+czlonkowski@users.noreply.github.com> Date: Fri, 24 Oct 2025 08:34:47 +0200 Subject: [PATCH] feat: Add auto-update node versions to autofixer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented comprehensive node version upgrade functionality with intelligent migration and breaking change detection. Key Features: - Smart version upgrades (typeversion-upgrade fix type) - Version migration guidance (version-migration fix type) - Auto-migration for Execute Workflow v1.0→v1.1 (adds inputFieldMapping) - Auto-migration for Webhook v2.0→v2.1 (generates webhookId) - Breaking changes registry with extensible patterns - AI-friendly post-update validation guidance - Confidence-based application (HIGH/MEDIUM/LOW) Architecture: - NodeVersionService: Version discovery and comparison - BreakingChangeDetector: Registry + dynamic schema comparison - NodeMigrationService: Smart property migrations - PostUpdateValidator: Step-by-step migration instructions - Enhanced database schema: node_versions, version_property_changes tables Services Created: - src/services/breaking-changes-registry.ts - src/services/breaking-change-detector.ts - src/services/node-version-service.ts - src/services/node-migration-service.ts - src/services/post-update-validator.ts Database Enhanced: - src/database/schema.sql (new version tracking tables) - src/database/node-repository.ts (15+ version query methods) Autofixer Integration: - src/services/workflow-auto-fixer.ts (async, new fix types) - src/mcp/handlers-n8n-manager.ts (await generateFixes) - src/mcp/tools-n8n-manager.ts (schema with new fix types) Documentation: - src/mcp/tool-docs/workflow_management/n8n-autofix-workflow.ts - CHANGELOG.md (comprehensive feature documentation) Testing: - Fixed all test scripts to await async generateFixes() - Added test workflow for Execute Workflow v1.0 upgrade testing Bug Fixes: - Fixed MCP tool schema enum to include new fix types - Fixed confidence type mapping (lowercase → uppercase) Conceived by Romuald Członkowski - www.aiadvisors.pl/en --- CHANGELOG.md | 74 +++ data/nodes.db | Bin 62623744 -> 62623744 bytes src/database/node-repository.ts | 278 ++++++++++++ src/database/schema.sql | 65 ++- src/mcp/handlers-n8n-manager.ts | 2 +- .../n8n-autofix-workflow.ts | 58 ++- src/mcp/tools-n8n-manager.ts | 2 +- src/scripts/test-autofix-workflow.ts | 6 +- src/scripts/test-node-suggestions.ts | 2 +- src/scripts/test-webhook-autofix.ts | 2 +- src/services/breaking-change-detector.ts | 321 +++++++++++++ src/services/breaking-changes-registry.ts | 315 +++++++++++++ src/services/node-migration-service.ts | 410 +++++++++++++++++ src/services/node-version-service.ts | 377 ++++++++++++++++ src/services/post-update-validator.ts | 423 ++++++++++++++++++ src/services/workflow-auto-fixer.ts | 215 ++++++++- 16 files changed, 2526 insertions(+), 24 deletions(-) create mode 100644 src/services/breaking-change-detector.ts create mode 100644 src/services/breaking-changes-registry.ts create mode 100644 src/services/node-migration-service.ts create mode 100644 src/services/node-version-service.ts create mode 100644 src/services/post-update-validator.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fe79984..ba5854a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### ✨ New Features + +**Auto-Update Node Versions with Smart Migration** + +Added comprehensive node version upgrade functionality to the autofixer, enabling automatic detection and migration of outdated node versions with intelligent breaking change handling. + +#### Key Features + +1. **Smart Version Upgrades** (`typeversion-upgrade` fix type): + - Automatically detects outdated node versions + - Applies intelligent migrations with auto-migratable property changes + - Handles well-known breaking changes (Execute Workflow v1.0→v1.1, Webhook v2.0→v2.1) + - Generates UUIDs and sensible defaults for new required fields + - HIGH confidence for non-breaking upgrades, MEDIUM for breaking changes with auto-migration + +2. **Version Migration Guidance** (`version-migration` fix type): + - Documents complex migrations requiring manual intervention + - Provides AI-friendly post-update guidance with step-by-step instructions + - Lists required actions by priority (CRITICAL, HIGH, MEDIUM, LOW) + - Documents behavior changes and their impact + - Estimates time required for manual migration steps + - MEDIUM/LOW confidence - requires review before applying + +3. **Breaking Changes Registry**: + - Centralized registry of known breaking changes across n8n nodes + - Example: Execute Workflow v1.1+ requires `inputFieldMapping` (auto-added) + - Example: Webhook v2.1+ requires `webhookId` field (auto-generated UUID) + - Extensible for future node version changes + +4. **Post-Update Validation**: + - Generates comprehensive migration reports for AI agents + - Includes required actions, deprecated properties, behavior changes + - Provides actionable migration steps with estimated time + - Helps AI agents understand what manual work is needed after auto-migration + +#### Architecture + +- **NodeVersionService**: Version discovery, comparison, upgrade path recommendation +- **BreakingChangeDetector**: Detects changes from registry and dynamic schema comparison +- **NodeMigrationService**: Applies smart migrations with confidence scoring +- **PostUpdateValidator**: Generates AI-friendly migration guidance +- **Enhanced Database Schema**: + - `node_versions` table - tracks all available versions per node + - `version_property_changes` table - detailed migration tracking + +#### Usage Example + +```typescript +// Preview all fixes including version upgrades +n8n_autofix_workflow({id: "wf_123"}) + +// Only upgrade versions with smart migrations +n8n_autofix_workflow({ + id: "wf_123", + fixTypes: ["typeversion-upgrade"], + applyFixes: true +}) + +// Get migration guidance for breaking changes +n8n_autofix_workflow({ + id: "wf_123", + fixTypes: ["version-migration"] +}) +``` + +#### Impact + +- Proactively keeps workflows up-to-date with latest node versions +- Reduces manual migration effort for Execute Workflow, Webhook, and other versioned nodes +- Provides clear guidance for AI agents on handling breaking changes +- Ensures workflows benefit from latest node features and bug fixes + +**Conceived by Romuald Członkowski - www.aiadvisors.pl/en** + ## [2.21.1] - 2025-10-23 ### 🐛 Bug Fixes diff --git a/data/nodes.db b/data/nodes.db index 6b94534e136bd2e05e4a0a567212e71018f2b636..83eb127a7276e6786bc63d8314936a13d084e2fc 100644 GIT binary patch delta 3599 zcmWmD`Gm9R=$rL59c z8LO;S&MI$Juqs-WtjbmutEyGas&3V=YFf3d+EyK_u2s*fZ#A$QT8*s6RuikK)y$$# zbE}2b(rRV3w%S;2t#(#>tAo|i>ST4cx>#MUZdP}zht<>SW%aiDSbeR2R)1@NHP9Ml z4Yr0@L#<)faBGA$(i&yi8f}fS##+JFIBUE$!J24IvL;(otf|&CYq~YVnrY3lW?OTt zxz;>uzO}$wXf3i9TOrmGYpJ!&T5hedR$8m9)z%tot+mctZ*8zPTAQrR))s54wawaY z?XY%QyR6;T9&4|)&)RPtunt;>ti#q3>!@|i`px>?I&Ph?PFkm|)7Ba5taZ-%!}`-Y zZ(Xo1T9>TL))nijb#6n3 zdTzb2URtlL*VY^Bt@X}&Z+);nTA!@X))(um_09Tj{jmPCeg-RWq5=^LK?sd72#atC zj|hl}NQjImh>B>4ju?oEScr`{h>LiLj|51FL`aMzNQz`gjuc3VR7j09NQ-nxj||9& zOvsEZ$ck*pjvUB|T*!?)$cuc)j{+!&LMV(PD2iezjuI$|QYeiwD2s9^j|!-WN~nw~ zsETT+jvAzL)i(rhycuc@VOu}SL!BkAc zbj-j^%))HU!CcJ4d@R61EW%=hU)aV-40~9oAz5HewStV+*!o8@6Ky zc48NHV-NOXANJz_4&o3F;|Px87=FX=IF1uIiBmX@GdPQL_yd39JTBlOF5xn+;3}@+ zI&R=E+{E9wh1>WC|Kbkr;vVkf0UqKJ9^(m~;u)Uf1zzG6UgHhk;vL@O13uytKI03% z;v2r>2mZs)z+i=9e}qC1LL&^qA{@da0wN+3A|nc-A{wG224W%>Vj~XXA|B!+0TLn+ z5+ezcA{mk+1yUjvQX>u0A|28r12Q5LG9wGJA{(+J2XZ18aw8A&A|LXj01BcI3Zn>$ zq8N&!1WKY5N}~+Qq8!Sj0xF^sDx(Ujq8h5B25O=fYNHP7q8{p_0UDwa8lwrCq8Wbm zZ;lpdiB@QhHfW1>Xpau)h)(E?F6fGG=#C!fiC*Z9KIn^n=#K#yh(Q>PAsC8b7>*Gb ziBT{{V+_V37~?P=6EG2zFd0)Y71J;sGcXggFdK6)7xOS53$PH2uoxj&f~8o7Sdhj@g?c!H;R zhUa*Jmw1KOc!Rfihxhn^kNAYo_=2zahVS@+|L`+Zu!7hhp%8@72!pT)hwzAih=_#9 zh=QnyhUkcan23egh=aI@hxkZ4JD1)*nhw`X^il~IjsDi4fhU%z+ zny7`^sDrwwhx%xMhG>MwXo99_hF|@gqXk-`6{x}qDpqX&AT z7kZ-)`l28DV*mzX5C&rihGH0oV+2NG6pYargRuz4IE=>xOvEHi#uQA&G)%_~%)~6r z#vIJWJj}-eEW{!#MhKQ*DVAY5R$wJoVKvrZE!JT@Hee$*VKcU1E4E=fc3>xVVK??* MZ_okl3ksS1KPGuVhyVZp delta 3599 zcmWmD(RvBFy6tngL@E20(2ifl!( zqFT|c=vE9XrWMPIZN;(TTJfy-Rst)bmB>nLC9#rP$*km73M-|R%1UjevC>-Utn^j} zE2EXk%4}t^vRc`!>{bpdrVc3vT9p(th!b`tG?C1YG^gG8e2`QrWSpg zSSOh_`dR(00oFik zkTuvEVhy#1S;MUn)=10NC~LGe#tN~!5YWI&2-Wj#|g8!J0R^|$rNdTc$ho?6eW=hh4B zrS-~sZN0JHTJNm))(7h!>tE}m^~w5deX+h;->mP}59>ecXNUqPC=j6#gwP0sun33n zh=7QQgvf}3sECH>h=G`hh1iILxQK`NNPvV$gv3aKq)3M3NP(0{h15ucv`B~a$bgK< zgv`i-tjLD!$bp>5h1|%4yvT?AD1d?}gu*C-q9}&qD1nkFh0-X4vM7i0sDO$HMkQ26 z6;wqvR7VZeL@m@t9n?iV)JFp}L?bjt6EwxI{>{)FEzlCJ&>C&f7VXd;9ncY-&>3CO z72VJsJMZw7yZy5127PSFc?EH6vHqaBQO%iD2&D!gkUVjVLT>aA|_!nreG?j zVLE1DCT3wa=3p-7VLldMAr@gVmS8ECVL4V{C01cI)?h8xVLdirBQ{|(wqPr^VLNtU zCw5^s_FymeVLuMwAP(U$j^HSc;W$p$riN zxP{yJ1ApQU?&2Qq;{hJxFZ_*1c#J1_if4F^7kG(Rc#SuBi+6aB5BLZF;v+ucGrr&} zzTrE5;6MBf3{fcdM<@g#G{PV(!XZ2&AR;0mGNK?Vq9HnBASPlVHsT;I;vqf~AR!VV zF_IuDk|8-#ASF^EHPRq0(jh%EAR{s%GqNBnvLQQiASZGmH}W7a@*zJ8pdbpNFp8ik zilI14pd?D6G|HeX%Aq_epdx}%36)U=RZ$JqQ3Ewm3$;-Pbx{xX(Ett62#wJMP4TOL zGc-pFv_vbkMjNz6JG4g!bVMg~Mi+ENH*`l2^h7W8Mj!M=KlH}{48$M|#t;m}Fbu~C zjD#@?qcH{{7>jWjj|rHFNtlc&n2Kqbjv1JVS(uGEn2ULsj|EtWMOcg_Sc+v>julvm zRalKRSc`R7j}6#}P1uYr*otk~jvd&EUD%C1*o%GGj{`V}LpY2hIErI9juSYE-|#z5 z;WW zUg8yA;|<>89p2*u{=vWah)?*8FZhaY_>Ld=4?jbND2V+L3PA{sFbIoq2#*Meh)9Tx zD2R$^h>jSDiCBn@IEagQh>rwFh(t(?BuI*6NRAXpiBw39G)RkdNRJH2h)l?gEXay% z$c`MyiCoByJjjcD$d3Xjh(aigA}EStD2@^+iBc$yGAN63D31!Lh+tGgWmG{`R6}*t zKuy#_ZPYCfi{OaEf&Cvoa(F(2625r#}?a=`p(FvW=1zph%-O&R* z(F?uN2Yt~G{V@OoF$jY(1Vb?l!!ZIQVT{6Pj6n#-VjRX}0w!V-CSwYwVj8An24-Rw zW@8TKVjkvW0TyBr7GnvPVi}fW1y*7eR$~p;Vjb3F12$q4He(C6VjH$&2X(^b diff --git a/src/database/node-repository.ts b/src/database/node-repository.ts index 845a190..33a63cb 100644 --- a/src/database/node-repository.ts +++ b/src/database/node-repository.ts @@ -462,4 +462,282 @@ export class NodeRepository { return undefined; } + + /** + * VERSION MANAGEMENT METHODS + * Methods for working with node_versions and version_property_changes tables + */ + + /** + * Save a specific node version to the database + */ + saveNodeVersion(versionData: { + nodeType: string; + version: string; + packageName: string; + displayName: string; + description?: string; + category?: string; + isCurrentMax?: boolean; + propertiesSchema?: any; + operations?: any; + credentialsRequired?: any; + outputs?: any; + minimumN8nVersion?: string; + breakingChanges?: any[]; + deprecatedProperties?: string[]; + addedProperties?: string[]; + releasedAt?: Date; + }): void { + const stmt = this.db.prepare(` + INSERT OR REPLACE INTO node_versions ( + node_type, version, package_name, display_name, description, + category, is_current_max, properties_schema, operations, + credentials_required, outputs, minimum_n8n_version, + breaking_changes, deprecated_properties, added_properties, + released_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + stmt.run( + versionData.nodeType, + versionData.version, + versionData.packageName, + versionData.displayName, + versionData.description || null, + versionData.category || null, + versionData.isCurrentMax ? 1 : 0, + versionData.propertiesSchema ? JSON.stringify(versionData.propertiesSchema) : null, + versionData.operations ? JSON.stringify(versionData.operations) : null, + versionData.credentialsRequired ? JSON.stringify(versionData.credentialsRequired) : null, + versionData.outputs ? JSON.stringify(versionData.outputs) : null, + versionData.minimumN8nVersion || null, + versionData.breakingChanges ? JSON.stringify(versionData.breakingChanges) : null, + versionData.deprecatedProperties ? JSON.stringify(versionData.deprecatedProperties) : null, + versionData.addedProperties ? JSON.stringify(versionData.addedProperties) : null, + versionData.releasedAt || null + ); + } + + /** + * Get all available versions for a specific node type + */ + getNodeVersions(nodeType: string): any[] { + const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); + + const rows = this.db.prepare(` + SELECT * FROM node_versions + WHERE node_type = ? + ORDER BY version DESC + `).all(normalizedType) as any[]; + + return rows.map(row => this.parseNodeVersionRow(row)); + } + + /** + * Get the latest (current max) version for a node type + */ + getLatestNodeVersion(nodeType: string): any | null { + const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); + + const row = this.db.prepare(` + SELECT * FROM node_versions + WHERE node_type = ? AND is_current_max = 1 + LIMIT 1 + `).get(normalizedType) as any; + + if (!row) return null; + return this.parseNodeVersionRow(row); + } + + /** + * Get a specific version of a node + */ + getNodeVersion(nodeType: string, version: string): any | null { + const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); + + const row = this.db.prepare(` + SELECT * FROM node_versions + WHERE node_type = ? AND version = ? + `).get(normalizedType, version) as any; + + if (!row) return null; + return this.parseNodeVersionRow(row); + } + + /** + * Save a property change between versions + */ + savePropertyChange(changeData: { + nodeType: string; + fromVersion: string; + toVersion: string; + propertyName: string; + changeType: 'added' | 'removed' | 'renamed' | 'type_changed' | 'requirement_changed' | 'default_changed'; + isBreaking?: boolean; + oldValue?: string; + newValue?: string; + migrationHint?: string; + autoMigratable?: boolean; + migrationStrategy?: any; + severity?: 'LOW' | 'MEDIUM' | 'HIGH'; + }): void { + const stmt = this.db.prepare(` + INSERT INTO version_property_changes ( + node_type, from_version, to_version, property_name, change_type, + is_breaking, old_value, new_value, migration_hint, auto_migratable, + migration_strategy, severity + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + stmt.run( + changeData.nodeType, + changeData.fromVersion, + changeData.toVersion, + changeData.propertyName, + changeData.changeType, + changeData.isBreaking ? 1 : 0, + changeData.oldValue || null, + changeData.newValue || null, + changeData.migrationHint || null, + changeData.autoMigratable ? 1 : 0, + changeData.migrationStrategy ? JSON.stringify(changeData.migrationStrategy) : null, + changeData.severity || 'MEDIUM' + ); + } + + /** + * Get property changes between two versions + */ + getPropertyChanges(nodeType: string, fromVersion: string, toVersion: string): any[] { + const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); + + const rows = this.db.prepare(` + SELECT * FROM version_property_changes + WHERE node_type = ? AND from_version = ? AND to_version = ? + ORDER BY severity DESC, property_name + `).all(normalizedType, fromVersion, toVersion) as any[]; + + return rows.map(row => this.parsePropertyChangeRow(row)); + } + + /** + * Get all breaking changes for upgrading from one version to another + * Can handle multi-step upgrades (e.g., 1.0 -> 2.0 via 1.5) + */ + getBreakingChanges(nodeType: string, fromVersion: string, toVersion?: string): any[] { + const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); + + let sql = ` + SELECT * FROM version_property_changes + WHERE node_type = ? AND is_breaking = 1 + `; + const params: any[] = [normalizedType]; + + if (toVersion) { + // Get changes between specific versions + sql += ` AND from_version >= ? AND to_version <= ?`; + params.push(fromVersion, toVersion); + } else { + // Get all breaking changes from this version onwards + sql += ` AND from_version >= ?`; + params.push(fromVersion); + } + + sql += ` ORDER BY from_version, to_version, severity DESC`; + + const rows = this.db.prepare(sql).all(...params) as any[]; + return rows.map(row => this.parsePropertyChangeRow(row)); + } + + /** + * Get auto-migratable changes for a version upgrade + */ + getAutoMigratableChanges(nodeType: string, fromVersion: string, toVersion: string): any[] { + const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); + + const rows = this.db.prepare(` + SELECT * FROM version_property_changes + WHERE node_type = ? + AND from_version = ? + AND to_version = ? + AND auto_migratable = 1 + ORDER BY severity DESC + `).all(normalizedType, fromVersion, toVersion) as any[]; + + return rows.map(row => this.parsePropertyChangeRow(row)); + } + + /** + * Check if a version upgrade path exists between two versions + */ + hasVersionUpgradePath(nodeType: string, fromVersion: string, toVersion: string): boolean { + const versions = this.getNodeVersions(nodeType); + if (versions.length === 0) return false; + + // Check if both versions exist + const fromExists = versions.some(v => v.version === fromVersion); + const toExists = versions.some(v => v.version === toVersion); + + return fromExists && toExists; + } + + /** + * Get count of nodes with multiple versions + */ + getVersionedNodesCount(): number { + const result = this.db.prepare(` + SELECT COUNT(DISTINCT node_type) as count + FROM node_versions + `).get() as any; + return result.count; + } + + /** + * Parse node version row from database + */ + private parseNodeVersionRow(row: any): any { + return { + id: row.id, + nodeType: row.node_type, + version: row.version, + packageName: row.package_name, + displayName: row.display_name, + description: row.description, + category: row.category, + isCurrentMax: Number(row.is_current_max) === 1, + propertiesSchema: row.properties_schema ? this.safeJsonParse(row.properties_schema, []) : null, + operations: row.operations ? this.safeJsonParse(row.operations, []) : null, + credentialsRequired: row.credentials_required ? this.safeJsonParse(row.credentials_required, []) : null, + outputs: row.outputs ? this.safeJsonParse(row.outputs, null) : null, + minimumN8nVersion: row.minimum_n8n_version, + breakingChanges: row.breaking_changes ? this.safeJsonParse(row.breaking_changes, []) : [], + deprecatedProperties: row.deprecated_properties ? this.safeJsonParse(row.deprecated_properties, []) : [], + addedProperties: row.added_properties ? this.safeJsonParse(row.added_properties, []) : [], + releasedAt: row.released_at, + createdAt: row.created_at + }; + } + + /** + * Parse property change row from database + */ + private parsePropertyChangeRow(row: any): any { + return { + id: row.id, + nodeType: row.node_type, + fromVersion: row.from_version, + toVersion: row.to_version, + propertyName: row.property_name, + changeType: row.change_type, + isBreaking: Number(row.is_breaking) === 1, + oldValue: row.old_value, + newValue: row.new_value, + migrationHint: row.migration_hint, + autoMigratable: Number(row.auto_migratable) === 1, + migrationStrategy: row.migration_strategy ? this.safeJsonParse(row.migration_strategy, null) : null, + severity: row.severity, + createdAt: row.created_at + }; + } } \ No newline at end of file diff --git a/src/database/schema.sql b/src/database/schema.sql index a988a94..05770cc 100644 --- a/src/database/schema.sql +++ b/src/database/schema.sql @@ -144,4 +144,67 @@ ORDER BY node_type, rank; -- Note: Template FTS5 tables are created conditionally at runtime if FTS5 is supported -- See template-repository.ts initializeFTS5() method --- Node FTS5 table (nodes_fts) is created above during schema initialization \ No newline at end of file +-- Node FTS5 table (nodes_fts) is created above during schema initialization + +-- Node versions table for tracking all available versions of each node +-- Enables version upgrade detection and migration +CREATE TABLE IF NOT EXISTS node_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + node_type TEXT NOT NULL, -- e.g., "n8n-nodes-base.executeWorkflow" + version TEXT NOT NULL, -- e.g., "1.0", "1.1", "2.0" + package_name TEXT NOT NULL, -- e.g., "n8n-nodes-base" + display_name TEXT NOT NULL, + description TEXT, + category TEXT, + is_current_max INTEGER DEFAULT 0, -- 1 if this is the latest version + properties_schema TEXT, -- JSON schema for this specific version + operations TEXT, -- JSON array of operations for this version + credentials_required TEXT, -- JSON array of required credentials + outputs TEXT, -- JSON array of output definitions + minimum_n8n_version TEXT, -- Minimum n8n version required (e.g., "1.0.0") + breaking_changes TEXT, -- JSON array of breaking changes from previous version + deprecated_properties TEXT, -- JSON array of removed/deprecated properties + added_properties TEXT, -- JSON array of newly added properties + released_at DATETIME, -- When this version was released + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(node_type, version), + FOREIGN KEY (node_type) REFERENCES nodes(node_type) ON DELETE CASCADE +); + +-- Indexes for version queries +CREATE INDEX IF NOT EXISTS idx_version_node_type ON node_versions(node_type); +CREATE INDEX IF NOT EXISTS idx_version_current_max ON node_versions(is_current_max); +CREATE INDEX IF NOT EXISTS idx_version_composite ON node_versions(node_type, version); + +-- Version property changes for detailed migration tracking +-- Records specific property-level changes between versions +CREATE TABLE IF NOT EXISTS version_property_changes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + node_type TEXT NOT NULL, + from_version TEXT NOT NULL, -- Version where change occurred (e.g., "1.0") + to_version TEXT NOT NULL, -- Target version (e.g., "1.1") + property_name TEXT NOT NULL, -- Property path (e.g., "parameters.inputFieldMapping") + change_type TEXT NOT NULL CHECK(change_type IN ( + 'added', -- Property added (may be required) + 'removed', -- Property removed/deprecated + 'renamed', -- Property renamed + 'type_changed', -- Property type changed + 'requirement_changed', -- Required → Optional or vice versa + 'default_changed' -- Default value changed + )), + is_breaking INTEGER DEFAULT 0, -- 1 if this is a breaking change + old_value TEXT, -- For renamed/type_changed: old property name or type + new_value TEXT, -- For renamed/type_changed: new property name or type + migration_hint TEXT, -- Human-readable migration guidance + auto_migratable INTEGER DEFAULT 0, -- 1 if can be automatically migrated + migration_strategy TEXT, -- JSON: strategy for auto-migration + severity TEXT CHECK(severity IN ('LOW', 'MEDIUM', 'HIGH')), -- Impact severity + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (node_type, from_version) REFERENCES node_versions(node_type, version) ON DELETE CASCADE +); + +-- Indexes for property change queries +CREATE INDEX IF NOT EXISTS idx_prop_changes_node ON version_property_changes(node_type); +CREATE INDEX IF NOT EXISTS idx_prop_changes_versions ON version_property_changes(node_type, from_version, to_version); +CREATE INDEX IF NOT EXISTS idx_prop_changes_breaking ON version_property_changes(is_breaking); +CREATE INDEX IF NOT EXISTS idx_prop_changes_auto ON version_property_changes(auto_migratable); \ No newline at end of file diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index c141962..47795e8 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -995,7 +995,7 @@ export async function handleAutofixWorkflow( // Generate fixes using WorkflowAutoFixer const autoFixer = new WorkflowAutoFixer(repository); - const fixResult = autoFixer.generateFixes( + const fixResult = await autoFixer.generateFixes( workflow, validationResult, allFormatIssues, diff --git a/src/mcp/tool-docs/workflow_management/n8n-autofix-workflow.ts b/src/mcp/tool-docs/workflow_management/n8n-autofix-workflow.ts index 86ced15..c3e946c 100644 --- a/src/mcp/tool-docs/workflow_management/n8n-autofix-workflow.ts +++ b/src/mcp/tool-docs/workflow_management/n8n-autofix-workflow.ts @@ -4,15 +4,17 @@ export const n8nAutofixWorkflowDoc: ToolDocumentation = { name: 'n8n_autofix_workflow', category: 'workflow_management', essentials: { - description: 'Automatically fix common workflow validation errors - expression formats, typeVersions, error outputs, webhook paths', + description: 'Automatically fix common workflow validation errors - expression formats, typeVersions, error outputs, webhook paths, and smart version upgrades', keyParameters: ['id', 'applyFixes'], example: 'n8n_autofix_workflow({id: "wf_abc123", applyFixes: false})', - performance: 'Network-dependent (200-1000ms) - fetches, validates, and optionally updates workflow', + performance: 'Network-dependent (200-1500ms) - fetches, validates, and optionally updates workflow with smart migrations', tips: [ 'Use applyFixes: false to preview changes before applying', 'Set confidenceThreshold to control fix aggressiveness (high/medium/low)', - 'Supports fixing expression formats, typeVersion issues, error outputs, node type corrections, and webhook paths', - 'High-confidence fixes (≥90%) are safe for auto-application' + 'Supports expression formats, typeVersion issues, error outputs, node corrections, webhook paths, AND version upgrades', + 'High-confidence fixes (≥90%) are safe for auto-application', + 'Version upgrades include smart migration with breaking change detection', + 'Post-update guidance provides AI-friendly step-by-step instructions for manual changes' ] }, full: { @@ -39,6 +41,20 @@ The auto-fixer can resolve: - Sets both 'path' parameter and 'webhookId' field to the same UUID - Ensures webhook nodes become functional with valid endpoints - High confidence fix as UUID generation is deterministic +6. **Smart Version Upgrades** (NEW): Proactively upgrades nodes to their latest versions: + - Detects outdated node versions and recommends upgrades + - Applies smart migrations with auto-migratable property changes + - Handles breaking changes intelligently (Execute Workflow v1.0→v1.1, Webhook v2.0→v2.1, etc.) + - Generates UUIDs for required fields (webhookId), sets sensible defaults + - HIGH confidence for non-breaking upgrades, MEDIUM for breaking changes with auto-migration + - Example: Execute Workflow v1.0→v1.1 adds inputFieldMapping automatically +7. **Version Migration Guidance** (NEW): Documents complex migrations requiring manual intervention: + - Identifies breaking changes that cannot be auto-migrated + - Provides AI-friendly post-update guidance with step-by-step instructions + - Lists required actions by priority (CRITICAL, HIGH, MEDIUM, LOW) + - Documents behavior changes and their impact + - Estimates time required for manual migration steps + - MEDIUM/LOW confidence - requires review before applying The tool uses a confidence-based system to ensure safe fixes: - **High (≥90%)**: Safe to auto-apply (exact matches, known patterns) @@ -60,7 +76,7 @@ Requires N8N_API_URL and N8N_API_KEY environment variables to be configured.`, fixTypes: { type: 'array', required: false, - description: 'Types of fixes to apply. Options: ["expression-format", "typeversion-correction", "error-output-config", "node-type-correction", "webhook-missing-path"]. Default: all types.' + description: 'Types of fixes to apply. Options: ["expression-format", "typeversion-correction", "error-output-config", "node-type-correction", "webhook-missing-path", "typeversion-upgrade", "version-migration"]. Default: all types. NEW: "typeversion-upgrade" for smart version upgrades, "version-migration" for complex migration guidance.' }, confidenceThreshold: { type: 'string', @@ -78,13 +94,21 @@ Requires N8N_API_URL and N8N_API_KEY environment variables to be configured.`, - fixes: Detailed list of individual fixes with before/after values - summary: Human-readable summary of fixes - stats: Statistics by fix type and confidence level -- applied: Boolean indicating if fixes were applied (when applyFixes: true)`, +- applied: Boolean indicating if fixes were applied (when applyFixes: true) +- postUpdateGuidance: (NEW) Array of AI-friendly migration guidance for version upgrades, including: + * Required actions by priority (CRITICAL, HIGH, MEDIUM, LOW) + * Deprecated properties to remove + * Behavior changes and their impact + * Step-by-step migration instructions + * Estimated time for manual changes`, examples: [ - 'n8n_autofix_workflow({id: "wf_abc123"}) - Preview all possible fixes', + 'n8n_autofix_workflow({id: "wf_abc123"}) - Preview all possible fixes including version upgrades', 'n8n_autofix_workflow({id: "wf_abc123", applyFixes: true}) - Apply all medium+ confidence fixes', 'n8n_autofix_workflow({id: "wf_abc123", applyFixes: true, confidenceThreshold: "high"}) - Only apply high-confidence fixes', 'n8n_autofix_workflow({id: "wf_abc123", fixTypes: ["expression-format"]}) - Only fix expression format issues', 'n8n_autofix_workflow({id: "wf_abc123", fixTypes: ["webhook-missing-path"]}) - Only fix webhook path issues', + 'n8n_autofix_workflow({id: "wf_abc123", fixTypes: ["typeversion-upgrade"]}) - NEW: Only upgrade node versions with smart migrations', + 'n8n_autofix_workflow({id: "wf_abc123", fixTypes: ["typeversion-upgrade", "version-migration"]}) - NEW: Upgrade versions and provide migration guidance', 'n8n_autofix_workflow({id: "wf_abc123", applyFixes: true, maxFixes: 10}) - Apply up to 10 fixes' ], useCases: [ @@ -94,16 +118,23 @@ Requires N8N_API_URL and N8N_API_KEY environment variables to be configured.`, 'Cleaning up workflows before production deployment', 'Batch fixing common issues across multiple workflows', 'Migrating workflows between n8n instances with different versions', - 'Repairing webhook nodes that lost their path configuration' + 'Repairing webhook nodes that lost their path configuration', + 'Upgrading Execute Workflow nodes from v1.0 to v1.1+ with automatic inputFieldMapping', + 'Modernizing webhook nodes to v2.1+ with stable webhookId fields', + 'Proactively keeping workflows up-to-date with latest node versions', + 'Getting detailed migration guidance for complex breaking changes' ], - performance: 'Depends on workflow size and number of issues. Preview mode: 200-500ms. Apply mode: 500-1000ms for medium workflows. Node similarity matching is cached for 5 minutes for improved performance on repeated validations.', + performance: 'Depends on workflow size and number of issues. Preview mode: 200-500ms. Apply mode: 500-1500ms for medium workflows with version upgrades. Node similarity matching and version metadata are cached for 5 minutes for improved performance on repeated validations.', bestPractices: [ 'Always preview fixes first (applyFixes: false) before applying', 'Start with high confidence threshold for production workflows', 'Review the fix summary to understand what changed', 'Test workflows after auto-fixing to ensure expected behavior', 'Use fixTypes parameter to target specific issue categories', - 'Keep maxFixes reasonable to avoid too many changes at once' + 'Keep maxFixes reasonable to avoid too many changes at once', + 'NEW: Review postUpdateGuidance for version upgrades - contains step-by-step migration instructions', + 'NEW: Test workflows after version upgrades - behavior may change even with successful auto-migration', + 'NEW: Apply version upgrades incrementally - start with high-confidence, non-breaking upgrades' ], pitfalls: [ 'Some fixes may change workflow behavior - always test after fixing', @@ -112,7 +143,12 @@ Requires N8N_API_URL and N8N_API_KEY environment variables to be configured.`, 'Node type corrections only work for known node types in the database', 'Cannot fix structural issues like missing nodes or invalid connections', 'TypeVersion downgrades might remove node features added in newer versions', - 'Generated webhook paths are new UUIDs - existing webhook URLs will change' + 'Generated webhook paths are new UUIDs - existing webhook URLs will change', + 'NEW: Version upgrades may introduce breaking changes - review postUpdateGuidance carefully', + 'NEW: Auto-migrated properties use sensible defaults which may not match your use case', + 'NEW: Execute Workflow v1.1+ requires explicit inputFieldMapping - automatic mapping uses empty array', + 'NEW: Some breaking changes cannot be auto-migrated and require manual intervention', + 'NEW: Version history is based on registry - unknown nodes cannot be upgraded' ], relatedTools: [ 'n8n_validate_workflow', diff --git a/src/mcp/tools-n8n-manager.ts b/src/mcp/tools-n8n-manager.ts index b45364f..4059c02 100644 --- a/src/mcp/tools-n8n-manager.ts +++ b/src/mcp/tools-n8n-manager.ts @@ -293,7 +293,7 @@ export const n8nManagementTools: ToolDefinition[] = [ description: 'Types of fixes to apply (default: all)', items: { type: 'string', - enum: ['expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path'] + enum: ['expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path', 'typeversion-upgrade', 'version-migration'] } }, confidenceThreshold: { diff --git a/src/scripts/test-autofix-workflow.ts b/src/scripts/test-autofix-workflow.ts index 6538455..83b4cf2 100644 --- a/src/scripts/test-autofix-workflow.ts +++ b/src/scripts/test-autofix-workflow.ts @@ -164,7 +164,7 @@ async function testAutofix() { // Step 3: Generate fixes in preview mode logger.info('\nStep 3: Generating fixes (preview mode)...'); const autoFixer = new WorkflowAutoFixer(); - const previewResult = autoFixer.generateFixes( + const previewResult = await autoFixer.generateFixes( testWorkflow as any, validationResult, allFormatIssues, @@ -210,7 +210,7 @@ async function testAutofix() { logger.info('\n\n=== Testing Different Confidence Thresholds ==='); for (const threshold of ['high', 'medium', 'low'] as const) { - const result = autoFixer.generateFixes( + const result = await autoFixer.generateFixes( testWorkflow as any, validationResult, allFormatIssues, @@ -227,7 +227,7 @@ async function testAutofix() { const fixTypes = ['expression-format', 'typeversion-correction', 'error-output-config'] as const; for (const fixType of fixTypes) { - const result = autoFixer.generateFixes( + const result = await autoFixer.generateFixes( testWorkflow as any, validationResult, allFormatIssues, diff --git a/src/scripts/test-node-suggestions.ts b/src/scripts/test-node-suggestions.ts index 28f872d..c0242ab 100644 --- a/src/scripts/test-node-suggestions.ts +++ b/src/scripts/test-node-suggestions.ts @@ -173,7 +173,7 @@ async function testNodeSimilarity() { console.log('='.repeat(60)); const autoFixer = new WorkflowAutoFixer(repository); - const fixResult = autoFixer.generateFixes( + const fixResult = await autoFixer.generateFixes( testWorkflow as any, validationResult, [], diff --git a/src/scripts/test-webhook-autofix.ts b/src/scripts/test-webhook-autofix.ts index b1c9b70..9590b3e 100644 --- a/src/scripts/test-webhook-autofix.ts +++ b/src/scripts/test-webhook-autofix.ts @@ -87,7 +87,7 @@ async function testWebhookAutofix() { // Step 2: Generate fixes (preview mode) logger.info('\nStep 2: Generating fixes in preview mode...'); - const fixResult = autoFixer.generateFixes( + const fixResult = await autoFixer.generateFixes( testWorkflow, validationResult, [], // No expression format issues to pass diff --git a/src/services/breaking-change-detector.ts b/src/services/breaking-change-detector.ts new file mode 100644 index 0000000..ac962ca --- /dev/null +++ b/src/services/breaking-change-detector.ts @@ -0,0 +1,321 @@ +/** + * Breaking Change Detector + * + * Detects breaking changes between node versions by: + * 1. Consulting the hardcoded breaking changes registry + * 2. Dynamically comparing property schemas between versions + * 3. Analyzing property requirement changes + * + * Used by the autofixer to intelligently upgrade node versions. + */ + +import { NodeRepository } from '../database/node-repository'; +import { + BREAKING_CHANGES_REGISTRY, + BreakingChange, + getBreakingChangesForNode, + getAllChangesForNode +} from './breaking-changes-registry'; + +export interface DetectedChange { + propertyName: string; + changeType: 'added' | 'removed' | 'renamed' | 'type_changed' | 'requirement_changed' | 'default_changed'; + isBreaking: boolean; + oldValue?: any; + newValue?: any; + migrationHint: string; + autoMigratable: boolean; + migrationStrategy?: any; + severity: 'LOW' | 'MEDIUM' | 'HIGH'; + source: 'registry' | 'dynamic'; // Where this change was detected +} + +export interface VersionUpgradeAnalysis { + nodeType: string; + fromVersion: string; + toVersion: string; + hasBreakingChanges: boolean; + changes: DetectedChange[]; + autoMigratableCount: number; + manualRequiredCount: number; + overallSeverity: 'LOW' | 'MEDIUM' | 'HIGH'; + recommendations: string[]; +} + +export class BreakingChangeDetector { + constructor(private nodeRepository: NodeRepository) {} + + /** + * Analyze a version upgrade and detect all changes + */ + async analyzeVersionUpgrade( + nodeType: string, + fromVersion: string, + toVersion: string + ): Promise { + // Get changes from registry + const registryChanges = this.getRegistryChanges(nodeType, fromVersion, toVersion); + + // Get dynamic changes by comparing schemas + const dynamicChanges = this.detectDynamicChanges(nodeType, fromVersion, toVersion); + + // Merge and deduplicate changes + const allChanges = this.mergeChanges(registryChanges, dynamicChanges); + + // Calculate statistics + const hasBreakingChanges = allChanges.some(c => c.isBreaking); + const autoMigratableCount = allChanges.filter(c => c.autoMigratable).length; + const manualRequiredCount = allChanges.filter(c => !c.autoMigratable).length; + + // Determine overall severity + const overallSeverity = this.calculateOverallSeverity(allChanges); + + // Generate recommendations + const recommendations = this.generateRecommendations(allChanges); + + return { + nodeType, + fromVersion, + toVersion, + hasBreakingChanges, + changes: allChanges, + autoMigratableCount, + manualRequiredCount, + overallSeverity, + recommendations + }; + } + + /** + * Get changes from the hardcoded registry + */ + private getRegistryChanges( + nodeType: string, + fromVersion: string, + toVersion: string + ): DetectedChange[] { + const registryChanges = getAllChangesForNode(nodeType, fromVersion, toVersion); + + return registryChanges.map(change => ({ + propertyName: change.propertyName, + changeType: change.changeType, + isBreaking: change.isBreaking, + oldValue: change.oldValue, + newValue: change.newValue, + migrationHint: change.migrationHint, + autoMigratable: change.autoMigratable, + migrationStrategy: change.migrationStrategy, + severity: change.severity, + source: 'registry' as const + })); + } + + /** + * Dynamically detect changes by comparing property schemas + */ + private detectDynamicChanges( + nodeType: string, + fromVersion: string, + toVersion: string + ): DetectedChange[] { + // Get both versions from the database + const oldVersionData = this.nodeRepository.getNodeVersion(nodeType, fromVersion); + const newVersionData = this.nodeRepository.getNodeVersion(nodeType, toVersion); + + if (!oldVersionData || !newVersionData) { + return []; // Can't detect dynamic changes without version data + } + + const changes: DetectedChange[] = []; + + // Compare properties schemas + const oldProps = this.flattenProperties(oldVersionData.propertiesSchema || []); + const newProps = this.flattenProperties(newVersionData.propertiesSchema || []); + + // Detect added properties + for (const propName of Object.keys(newProps)) { + if (!oldProps[propName]) { + const prop = newProps[propName]; + const isRequired = prop.required === true; + + changes.push({ + propertyName: propName, + changeType: 'added', + isBreaking: isRequired, // Breaking if required + newValue: prop.type || 'unknown', + migrationHint: isRequired + ? `Property "${propName}" is now required in v${toVersion}. Provide a value to prevent validation errors.` + : `Property "${propName}" was added in v${toVersion}. Optional parameter, safe to ignore if not needed.`, + autoMigratable: !isRequired, // Can auto-add with default if not required + migrationStrategy: !isRequired + ? { + type: 'add_property', + defaultValue: prop.default || null + } + : undefined, + severity: isRequired ? 'HIGH' : 'LOW', + source: 'dynamic' + }); + } + } + + // Detect removed properties + for (const propName of Object.keys(oldProps)) { + if (!newProps[propName]) { + changes.push({ + propertyName: propName, + changeType: 'removed', + isBreaking: true, // Removal is always breaking + oldValue: oldProps[propName].type || 'unknown', + migrationHint: `Property "${propName}" was removed in v${toVersion}. Remove this property from your configuration.`, + autoMigratable: true, // Can auto-remove + migrationStrategy: { + type: 'remove_property' + }, + severity: 'MEDIUM', + source: 'dynamic' + }); + } + } + + // Detect requirement changes + for (const propName of Object.keys(newProps)) { + if (oldProps[propName]) { + const oldRequired = oldProps[propName].required === true; + const newRequired = newProps[propName].required === true; + + if (oldRequired !== newRequired) { + changes.push({ + propertyName: propName, + changeType: 'requirement_changed', + isBreaking: newRequired && !oldRequired, // Breaking if became required + oldValue: oldRequired ? 'required' : 'optional', + newValue: newRequired ? 'required' : 'optional', + migrationHint: newRequired + ? `Property "${propName}" is now required in v${toVersion}. Ensure a value is provided.` + : `Property "${propName}" is now optional in v${toVersion}.`, + autoMigratable: false, // Requirement changes need manual review + severity: newRequired ? 'HIGH' : 'LOW', + source: 'dynamic' + }); + } + } + } + + return changes; + } + + /** + * Flatten nested properties into a map for easy comparison + */ + private flattenProperties(properties: any[], prefix: string = ''): Record { + const flat: Record = {}; + + for (const prop of properties) { + if (!prop.name && !prop.displayName) continue; + + const propName = prop.name || prop.displayName; + const fullPath = prefix ? `${prefix}.${propName}` : propName; + + flat[fullPath] = prop; + + // Recursively flatten nested options + if (prop.options && Array.isArray(prop.options)) { + Object.assign(flat, this.flattenProperties(prop.options, fullPath)); + } + } + + return flat; + } + + /** + * Merge registry and dynamic changes, avoiding duplicates + */ + private mergeChanges( + registryChanges: DetectedChange[], + dynamicChanges: DetectedChange[] + ): DetectedChange[] { + const merged = [...registryChanges]; + + // Add dynamic changes that aren't already in registry + for (const dynamicChange of dynamicChanges) { + const existsInRegistry = registryChanges.some( + rc => rc.propertyName === dynamicChange.propertyName && + rc.changeType === dynamicChange.changeType + ); + + if (!existsInRegistry) { + merged.push(dynamicChange); + } + } + + // Sort by severity (HIGH -> MEDIUM -> LOW) + const severityOrder = { HIGH: 0, MEDIUM: 1, LOW: 2 }; + merged.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]); + + return merged; + } + + /** + * Calculate overall severity of the upgrade + */ + private calculateOverallSeverity(changes: DetectedChange[]): 'LOW' | 'MEDIUM' | 'HIGH' { + if (changes.some(c => c.severity === 'HIGH')) return 'HIGH'; + if (changes.some(c => c.severity === 'MEDIUM')) return 'MEDIUM'; + return 'LOW'; + } + + /** + * Generate actionable recommendations for the upgrade + */ + private generateRecommendations(changes: DetectedChange[]): string[] { + const recommendations: string[] = []; + + const breakingChanges = changes.filter(c => c.isBreaking); + const autoMigratable = changes.filter(c => c.autoMigratable); + const manualRequired = changes.filter(c => !c.autoMigratable); + + if (breakingChanges.length === 0) { + recommendations.push('✓ No breaking changes detected. This upgrade should be safe.'); + } else { + recommendations.push( + `⚠ ${breakingChanges.length} breaking change(s) detected. Review carefully before applying.` + ); + } + + if (autoMigratable.length > 0) { + recommendations.push( + `✓ ${autoMigratable.length} change(s) can be automatically migrated.` + ); + } + + if (manualRequired.length > 0) { + recommendations.push( + `✋ ${manualRequired.length} change(s) require manual intervention.` + ); + + // List specific manual changes + for (const change of manualRequired) { + recommendations.push(` - ${change.propertyName}: ${change.migrationHint}`); + } + } + + return recommendations; + } + + /** + * Quick check: does this upgrade have breaking changes? + */ + hasBreakingChanges(nodeType: string, fromVersion: string, toVersion: string): boolean { + const registryChanges = getBreakingChangesForNode(nodeType, fromVersion, toVersion); + return registryChanges.length > 0; + } + + /** + * Get simple list of property names that changed + */ + getChangedProperties(nodeType: string, fromVersion: string, toVersion: string): string[] { + const registryChanges = getAllChangesForNode(nodeType, fromVersion, toVersion); + return registryChanges.map(c => c.propertyName); + } +} diff --git a/src/services/breaking-changes-registry.ts b/src/services/breaking-changes-registry.ts new file mode 100644 index 0000000..33e96c4 --- /dev/null +++ b/src/services/breaking-changes-registry.ts @@ -0,0 +1,315 @@ +/** + * Breaking Changes Registry + * + * Central registry of known breaking changes between node versions. + * Used by the autofixer to detect and migrate version upgrades intelligently. + * + * Each entry defines: + * - Which versions are affected + * - What properties changed + * - Whether it's auto-migratable + * - Migration strategies and hints + */ + +export interface BreakingChange { + nodeType: string; + fromVersion: string; + toVersion: string; + propertyName: string; + changeType: 'added' | 'removed' | 'renamed' | 'type_changed' | 'requirement_changed' | 'default_changed'; + isBreaking: boolean; + oldValue?: string; + newValue?: string; + migrationHint: string; + autoMigratable: boolean; + migrationStrategy?: { + type: 'add_property' | 'remove_property' | 'rename_property' | 'set_default'; + defaultValue?: any; + sourceProperty?: string; + targetProperty?: string; + }; + severity: 'LOW' | 'MEDIUM' | 'HIGH'; +} + +/** + * Registry of known breaking changes across all n8n nodes + */ +export const BREAKING_CHANGES_REGISTRY: BreakingChange[] = [ + // ========================================== + // Execute Workflow Node + // ========================================== + { + nodeType: 'n8n-nodes-base.executeWorkflow', + fromVersion: '1.0', + toVersion: '1.1', + propertyName: 'parameters.inputFieldMapping', + changeType: 'added', + isBreaking: true, + migrationHint: 'In v1.1+, the Execute Workflow node requires explicit field mapping to pass data to sub-workflows. Add an "inputFieldMapping" object with "mappings" array defining how to map fields from parent to child workflow.', + autoMigratable: true, + migrationStrategy: { + type: 'add_property', + defaultValue: { + mappings: [] + } + }, + severity: 'HIGH' + }, + { + nodeType: 'n8n-nodes-base.executeWorkflow', + fromVersion: '1.0', + toVersion: '1.1', + propertyName: 'parameters.mode', + changeType: 'requirement_changed', + isBreaking: false, + migrationHint: 'The "mode" parameter behavior changed in v1.1. Default is now "static" instead of "list". Ensure your workflow ID specification matches the selected mode.', + autoMigratable: false, + severity: 'MEDIUM' + }, + + // ========================================== + // Webhook Node + // ========================================== + { + nodeType: 'n8n-nodes-base.webhook', + fromVersion: '2.0', + toVersion: '2.1', + propertyName: 'webhookId', + changeType: 'added', + isBreaking: true, + migrationHint: 'In v2.1+, webhooks require a unique "webhookId" field in addition to the path. This ensures webhook persistence across workflow updates. A UUID will be auto-generated if not provided.', + autoMigratable: true, + migrationStrategy: { + type: 'add_property', + defaultValue: null // Will be generated as UUID at runtime + }, + severity: 'HIGH' + }, + { + nodeType: 'n8n-nodes-base.webhook', + fromVersion: '1.0', + toVersion: '2.0', + propertyName: 'parameters.path', + changeType: 'requirement_changed', + isBreaking: true, + migrationHint: 'In v2.0+, the webhook path must be explicitly defined and cannot be empty. Ensure a valid path is set.', + autoMigratable: false, + severity: 'HIGH' + }, + { + nodeType: 'n8n-nodes-base.webhook', + fromVersion: '1.0', + toVersion: '2.0', + propertyName: 'parameters.responseMode', + changeType: 'added', + isBreaking: false, + migrationHint: 'v2.0 introduces a "responseMode" parameter to control how the webhook responds. Default is "onReceived" (immediate response). Use "lastNode" to wait for workflow completion.', + autoMigratable: true, + migrationStrategy: { + type: 'add_property', + defaultValue: 'onReceived' + }, + severity: 'LOW' + }, + + // ========================================== + // HTTP Request Node + // ========================================== + { + nodeType: 'n8n-nodes-base.httpRequest', + fromVersion: '4.1', + toVersion: '4.2', + propertyName: 'parameters.sendBody', + changeType: 'requirement_changed', + isBreaking: false, + migrationHint: 'In v4.2+, "sendBody" must be explicitly set to true for POST/PUT/PATCH requests to include a body. Previous versions had implicit body sending.', + autoMigratable: true, + migrationStrategy: { + type: 'add_property', + defaultValue: true + }, + severity: 'MEDIUM' + }, + + // ========================================== + // Code Node (JavaScript) + // ========================================== + { + nodeType: 'n8n-nodes-base.code', + fromVersion: '1.0', + toVersion: '2.0', + propertyName: 'parameters.mode', + changeType: 'added', + isBreaking: false, + migrationHint: 'v2.0 introduces execution modes: "runOnceForAllItems" (default) and "runOnceForEachItem". The default mode processes all items at once, which may differ from v1.0 behavior.', + autoMigratable: true, + migrationStrategy: { + type: 'add_property', + defaultValue: 'runOnceForAllItems' + }, + severity: 'MEDIUM' + }, + + // ========================================== + // Schedule Trigger Node + // ========================================== + { + nodeType: 'n8n-nodes-base.scheduleTrigger', + fromVersion: '1.0', + toVersion: '1.1', + propertyName: 'parameters.rule.interval', + changeType: 'type_changed', + isBreaking: true, + oldValue: 'string', + newValue: 'array', + migrationHint: 'In v1.1+, the interval parameter changed from a single string to an array of interval objects. Convert your single interval to an array format: [{field: "hours", value: 1}]', + autoMigratable: false, + severity: 'HIGH' + }, + + // ========================================== + // Error Handling (Global Change) + // ========================================== + { + nodeType: '*', // Applies to all nodes + fromVersion: '1.0', + toVersion: '2.0', + propertyName: 'continueOnFail', + changeType: 'removed', + isBreaking: false, + migrationHint: 'The "continueOnFail" property is deprecated. Use "onError" instead with value "continueErrorOutput" or "continueRegularOutput".', + autoMigratable: true, + migrationStrategy: { + type: 'rename_property', + sourceProperty: 'continueOnFail', + targetProperty: 'onError', + defaultValue: 'continueErrorOutput' + }, + severity: 'MEDIUM' + } +]; + +/** + * Get breaking changes for a specific node type and version upgrade + */ +export function getBreakingChangesForNode( + nodeType: string, + fromVersion: string, + toVersion: string +): BreakingChange[] { + return BREAKING_CHANGES_REGISTRY.filter(change => { + // Match exact node type or wildcard (*) + const nodeMatches = change.nodeType === nodeType || change.nodeType === '*'; + + // Check if version range matches + const versionMatches = + compareVersions(fromVersion, change.fromVersion) >= 0 && + compareVersions(toVersion, change.toVersion) <= 0; + + return nodeMatches && versionMatches && change.isBreaking; + }); +} + +/** + * Get all changes (breaking and non-breaking) for a version upgrade + */ +export function getAllChangesForNode( + nodeType: string, + fromVersion: string, + toVersion: string +): BreakingChange[] { + return BREAKING_CHANGES_REGISTRY.filter(change => { + const nodeMatches = change.nodeType === nodeType || change.nodeType === '*'; + const versionMatches = + compareVersions(fromVersion, change.fromVersion) >= 0 && + compareVersions(toVersion, change.toVersion) <= 0; + + return nodeMatches && versionMatches; + }); +} + +/** + * Get auto-migratable changes for a version upgrade + */ +export function getAutoMigratableChanges( + nodeType: string, + fromVersion: string, + toVersion: string +): BreakingChange[] { + return getAllChangesForNode(nodeType, fromVersion, toVersion).filter( + change => change.autoMigratable + ); +} + +/** + * Check if a specific node has known breaking changes for a version upgrade + */ +export function hasBreakingChanges( + nodeType: string, + fromVersion: string, + toVersion: string +): boolean { + return getBreakingChangesForNode(nodeType, fromVersion, toVersion).length > 0; +} + +/** + * Get migration hints for a version upgrade + */ +export function getMigrationHints( + nodeType: string, + fromVersion: string, + toVersion: string +): string[] { + const changes = getAllChangesForNode(nodeType, fromVersion, toVersion); + return changes.map(change => change.migrationHint); +} + +/** + * Simple version comparison + * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2 + */ +function compareVersions(v1: string, v2: string): number { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const p1 = parts1[i] || 0; + const p2 = parts2[i] || 0; + + if (p1 < p2) return -1; + if (p1 > p2) return 1; + } + + return 0; +} + +/** + * Get nodes with known version migrations + */ +export function getNodesWithVersionMigrations(): string[] { + const nodeTypes = new Set(); + + BREAKING_CHANGES_REGISTRY.forEach(change => { + if (change.nodeType !== '*') { + nodeTypes.add(change.nodeType); + } + }); + + return Array.from(nodeTypes); +} + +/** + * Get all versions tracked for a specific node + */ +export function getTrackedVersionsForNode(nodeType: string): string[] { + const versions = new Set(); + + BREAKING_CHANGES_REGISTRY + .filter(change => change.nodeType === nodeType || change.nodeType === '*') + .forEach(change => { + versions.add(change.fromVersion); + versions.add(change.toVersion); + }); + + return Array.from(versions).sort((a, b) => compareVersions(a, b)); +} diff --git a/src/services/node-migration-service.ts b/src/services/node-migration-service.ts new file mode 100644 index 0000000..fd0bd19 --- /dev/null +++ b/src/services/node-migration-service.ts @@ -0,0 +1,410 @@ +/** + * Node Migration Service + * + * Handles smart auto-migration of node configurations during version upgrades. + * Applies migration strategies from the breaking changes registry and detectors. + * + * Migration strategies: + * - add_property: Add new required/optional properties with defaults + * - remove_property: Remove deprecated properties + * - rename_property: Rename properties that changed names + * - set_default: Set default values for properties + */ + +import { v4 as uuidv4 } from 'uuid'; +import { BreakingChangeDetector, DetectedChange } from './breaking-change-detector'; +import { NodeVersionService } from './node-version-service'; + +export interface MigrationResult { + success: boolean; + nodeId: string; + nodeName: string; + fromVersion: string; + toVersion: string; + appliedMigrations: AppliedMigration[]; + remainingIssues: string[]; + confidence: 'HIGH' | 'MEDIUM' | 'LOW'; + updatedNode: any; // The migrated node configuration +} + +export interface AppliedMigration { + propertyName: string; + action: string; + oldValue?: any; + newValue?: any; + description: string; +} + +export class NodeMigrationService { + constructor( + private versionService: NodeVersionService, + private breakingChangeDetector: BreakingChangeDetector + ) {} + + /** + * Migrate a node from its current version to a target version + */ + async migrateNode( + node: any, + fromVersion: string, + toVersion: string + ): Promise { + const nodeId = node.id || 'unknown'; + const nodeName = node.name || 'Unknown Node'; + const nodeType = node.type; + + // Analyze the version upgrade + const analysis = await this.breakingChangeDetector.analyzeVersionUpgrade( + nodeType, + fromVersion, + toVersion + ); + + // Start with a copy of the node + const migratedNode = JSON.parse(JSON.stringify(node)); + + // Apply the version update + migratedNode.typeVersion = this.parseVersion(toVersion); + + const appliedMigrations: AppliedMigration[] = []; + const remainingIssues: string[] = []; + + // Apply auto-migratable changes + for (const change of analysis.changes.filter(c => c.autoMigratable)) { + const migration = this.applyMigration(migratedNode, change); + + if (migration) { + appliedMigrations.push(migration); + } + } + + // Collect remaining manual issues + for (const change of analysis.changes.filter(c => !c.autoMigratable)) { + remainingIssues.push( + `Manual action required for "${change.propertyName}": ${change.migrationHint}` + ); + } + + // Determine confidence based on remaining issues + let confidence: 'HIGH' | 'MEDIUM' | 'LOW' = 'HIGH'; + + if (remainingIssues.length > 0) { + confidence = remainingIssues.length > 3 ? 'LOW' : 'MEDIUM'; + } + + return { + success: remainingIssues.length === 0, + nodeId, + nodeName, + fromVersion, + toVersion, + appliedMigrations, + remainingIssues, + confidence, + updatedNode: migratedNode + }; + } + + /** + * Apply a single migration change to a node + */ + private applyMigration(node: any, change: DetectedChange): AppliedMigration | null { + if (!change.migrationStrategy) return null; + + const { type, defaultValue, sourceProperty, targetProperty } = change.migrationStrategy; + + switch (type) { + case 'add_property': + return this.addProperty(node, change.propertyName, defaultValue, change); + + case 'remove_property': + return this.removeProperty(node, change.propertyName, change); + + case 'rename_property': + return this.renameProperty(node, sourceProperty!, targetProperty!, change); + + case 'set_default': + return this.setDefault(node, change.propertyName, defaultValue, change); + + default: + return null; + } + } + + /** + * Add a new property to the node configuration + */ + private addProperty( + node: any, + propertyPath: string, + defaultValue: any, + change: DetectedChange + ): AppliedMigration { + const value = this.resolveDefaultValue(propertyPath, defaultValue, node); + + // Handle nested property paths (e.g., "parameters.inputFieldMapping") + const parts = propertyPath.split('.'); + let target = node; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!target[part]) { + target[part] = {}; + } + target = target[part]; + } + + const finalKey = parts[parts.length - 1]; + target[finalKey] = value; + + return { + propertyName: propertyPath, + action: 'Added property', + newValue: value, + description: `Added "${propertyPath}" with default value` + }; + } + + /** + * Remove a deprecated property from the node configuration + */ + private removeProperty( + node: any, + propertyPath: string, + change: DetectedChange + ): AppliedMigration | null { + const parts = propertyPath.split('.'); + let target = node; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!target[part]) return null; // Property doesn't exist + target = target[part]; + } + + const finalKey = parts[parts.length - 1]; + const oldValue = target[finalKey]; + + if (oldValue !== undefined) { + delete target[finalKey]; + + return { + propertyName: propertyPath, + action: 'Removed property', + oldValue, + description: `Removed deprecated property "${propertyPath}"` + }; + } + + return null; + } + + /** + * Rename a property (move value from old name to new name) + */ + private renameProperty( + node: any, + sourcePath: string, + targetPath: string, + change: DetectedChange + ): AppliedMigration | null { + // Get old value + const sourceParts = sourcePath.split('.'); + let sourceTarget = node; + + for (let i = 0; i < sourceParts.length - 1; i++) { + if (!sourceTarget[sourceParts[i]]) return null; + sourceTarget = sourceTarget[sourceParts[i]]; + } + + const sourceKey = sourceParts[sourceParts.length - 1]; + const oldValue = sourceTarget[sourceKey]; + + if (oldValue === undefined) return null; // Source doesn't exist + + // Set new value + const targetParts = targetPath.split('.'); + let targetTarget = node; + + for (let i = 0; i < targetParts.length - 1; i++) { + if (!targetTarget[targetParts[i]]) { + targetTarget[targetParts[i]] = {}; + } + targetTarget = targetTarget[targetParts[i]]; + } + + const targetKey = targetParts[targetParts.length - 1]; + targetTarget[targetKey] = oldValue; + + // Remove old value + delete sourceTarget[sourceKey]; + + return { + propertyName: targetPath, + action: 'Renamed property', + oldValue: `${sourcePath}: ${JSON.stringify(oldValue)}`, + newValue: `${targetPath}: ${JSON.stringify(oldValue)}`, + description: `Renamed "${sourcePath}" to "${targetPath}"` + }; + } + + /** + * Set a default value for a property + */ + private setDefault( + node: any, + propertyPath: string, + defaultValue: any, + change: DetectedChange + ): AppliedMigration | null { + const parts = propertyPath.split('.'); + let target = node; + + for (let i = 0; i < parts.length - 1; i++) { + if (!target[parts[i]]) { + target[parts[i]] = {}; + } + target = target[parts[i]]; + } + + const finalKey = parts[parts.length - 1]; + + // Only set if not already defined + if (target[finalKey] === undefined) { + const value = this.resolveDefaultValue(propertyPath, defaultValue, node); + target[finalKey] = value; + + return { + propertyName: propertyPath, + action: 'Set default value', + newValue: value, + description: `Set default value for "${propertyPath}"` + }; + } + + return null; + } + + /** + * Resolve default value with special handling for certain property types + */ + private resolveDefaultValue(propertyPath: string, defaultValue: any, node: any): any { + // Special case: webhookId needs a UUID + if (propertyPath === 'webhookId' || propertyPath.endsWith('.webhookId')) { + return uuidv4(); + } + + // Special case: webhook path needs a unique value + if (propertyPath === 'path' || propertyPath.endsWith('.path')) { + if (node.type === 'n8n-nodes-base.webhook') { + return `/webhook-${Date.now()}`; + } + } + + // Return provided default or null + return defaultValue !== null && defaultValue !== undefined ? defaultValue : null; + } + + /** + * Parse version string to number (for typeVersion field) + */ + private parseVersion(version: string): number { + const parts = version.split('.').map(Number); + + // Handle versions like "1.1" -> 1.1, "2.0" -> 2 + if (parts.length === 1) return parts[0]; + if (parts.length === 2) return parts[0] + parts[1] / 10; + + // For more complex versions, just use first number + return parts[0]; + } + + /** + * Validate that a migrated node is valid + */ + async validateMigratedNode(node: any, nodeType: string): Promise<{ + valid: boolean; + errors: string[]; + warnings: string[]; + }> { + const errors: string[] = []; + const warnings: string[] = []; + + // Basic validation + if (!node.typeVersion) { + errors.push('Missing typeVersion after migration'); + } + + if (!node.parameters) { + errors.push('Missing parameters object'); + } + + // Check for common issues + if (nodeType === 'n8n-nodes-base.webhook') { + if (!node.parameters?.path) { + errors.push('Webhook node missing required "path" parameter'); + } + if (node.typeVersion >= 2.1 && !node.webhookId) { + warnings.push('Webhook v2.1+ typically requires webhookId'); + } + } + + if (nodeType === 'n8n-nodes-base.executeWorkflow') { + if (node.typeVersion >= 1.1 && !node.parameters?.inputFieldMapping) { + errors.push('Execute Workflow v1.1+ requires inputFieldMapping'); + } + } + + return { + valid: errors.length === 0, + errors, + warnings + }; + } + + /** + * Batch migrate multiple nodes in a workflow + */ + async migrateWorkflowNodes( + workflow: any, + targetVersions: Record // nodeId -> targetVersion + ): Promise<{ + success: boolean; + results: MigrationResult[]; + overallConfidence: 'HIGH' | 'MEDIUM' | 'LOW'; + }> { + const results: MigrationResult[] = []; + + for (const node of workflow.nodes || []) { + const targetVersion = targetVersions[node.id]; + + if (targetVersion && node.typeVersion) { + const currentVersion = node.typeVersion.toString(); + + const result = await this.migrateNode(node, currentVersion, targetVersion); + results.push(result); + + // Update node in place + Object.assign(node, result.updatedNode); + } + } + + // Calculate overall confidence + const confidences = results.map(r => r.confidence); + let overallConfidence: 'HIGH' | 'MEDIUM' | 'LOW' = 'HIGH'; + + if (confidences.includes('LOW')) { + overallConfidence = 'LOW'; + } else if (confidences.includes('MEDIUM')) { + overallConfidence = 'MEDIUM'; + } + + const success = results.every(r => r.success); + + return { + success, + results, + overallConfidence + }; + } +} diff --git a/src/services/node-version-service.ts b/src/services/node-version-service.ts new file mode 100644 index 0000000..f9bae79 --- /dev/null +++ b/src/services/node-version-service.ts @@ -0,0 +1,377 @@ +/** + * Node Version Service + * + * Central service for node version discovery, comparison, and upgrade path recommendation. + * Provides caching for performance and integrates with the database and breaking change detector. + */ + +import { NodeRepository } from '../database/node-repository'; +import { BreakingChangeDetector } from './breaking-change-detector'; + +export interface NodeVersion { + nodeType: string; + version: string; + packageName: string; + displayName: string; + isCurrentMax: boolean; + minimumN8nVersion?: string; + breakingChanges: any[]; + deprecatedProperties: string[]; + addedProperties: string[]; + releasedAt?: Date; +} + +export interface VersionComparison { + nodeType: string; + currentVersion: string; + latestVersion: string; + isOutdated: boolean; + versionGap: number; // How many versions behind + hasBreakingChanges: boolean; + recommendUpgrade: boolean; + confidence: 'HIGH' | 'MEDIUM' | 'LOW'; + reason: string; +} + +export interface UpgradePath { + nodeType: string; + fromVersion: string; + toVersion: string; + direct: boolean; // Can upgrade directly or needs intermediate steps + intermediateVersions: string[]; // If multi-step upgrade needed + totalBreakingChanges: number; + autoMigratableChanges: number; + manualRequiredChanges: number; + estimatedEffort: 'LOW' | 'MEDIUM' | 'HIGH'; + steps: UpgradeStep[]; +} + +export interface UpgradeStep { + fromVersion: string; + toVersion: string; + breakingChanges: number; + migrationHints: string[]; +} + +/** + * Node Version Service with caching + */ +export class NodeVersionService { + private versionCache: Map = new Map(); + private cacheTTL: number = 5 * 60 * 1000; // 5 minutes + private cacheTimestamps: Map = new Map(); + + constructor( + private nodeRepository: NodeRepository, + private breakingChangeDetector: BreakingChangeDetector + ) {} + + /** + * Get all available versions for a node type + */ + getAvailableVersions(nodeType: string): NodeVersion[] { + // Check cache first + const cached = this.getCachedVersions(nodeType); + if (cached) return cached; + + // Query from database + const versions = this.nodeRepository.getNodeVersions(nodeType); + + // Cache the result + this.cacheVersions(nodeType, versions); + + return versions; + } + + /** + * Get the latest available version for a node type + */ + getLatestVersion(nodeType: string): string | null { + const versions = this.getAvailableVersions(nodeType); + + if (versions.length === 0) { + // Fallback to main nodes table + const node = this.nodeRepository.getNode(nodeType); + return node?.version || null; + } + + // Find version marked as current max + const maxVersion = versions.find(v => v.isCurrentMax); + if (maxVersion) return maxVersion.version; + + // Fallback: sort and get highest + const sorted = versions.sort((a, b) => this.compareVersions(b.version, a.version)); + return sorted[0]?.version || null; + } + + /** + * Compare a node's current version against the latest available + */ + compareVersions(currentVersion: string, latestVersion: string): number { + const parts1 = currentVersion.split('.').map(Number); + const parts2 = latestVersion.split('.').map(Number); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const p1 = parts1[i] || 0; + const p2 = parts2[i] || 0; + + if (p1 < p2) return -1; + if (p1 > p2) return 1; + } + + return 0; + } + + /** + * Analyze if a node version is outdated and should be upgraded + */ + analyzeVersion(nodeType: string, currentVersion: string): VersionComparison { + const latestVersion = this.getLatestVersion(nodeType); + + if (!latestVersion) { + return { + nodeType, + currentVersion, + latestVersion: currentVersion, + isOutdated: false, + versionGap: 0, + hasBreakingChanges: false, + recommendUpgrade: false, + confidence: 'HIGH', + reason: 'No version information available. Using current version.' + }; + } + + const comparison = this.compareVersions(currentVersion, latestVersion); + const isOutdated = comparison < 0; + + if (!isOutdated) { + return { + nodeType, + currentVersion, + latestVersion, + isOutdated: false, + versionGap: 0, + hasBreakingChanges: false, + recommendUpgrade: false, + confidence: 'HIGH', + reason: 'Node is already at the latest version.' + }; + } + + // Calculate version gap + const versionGap = this.calculateVersionGap(currentVersion, latestVersion); + + // Check for breaking changes + const hasBreakingChanges = this.breakingChangeDetector.hasBreakingChanges( + nodeType, + currentVersion, + latestVersion + ); + + // Determine upgrade recommendation and confidence + let recommendUpgrade = true; + let confidence: 'HIGH' | 'MEDIUM' | 'LOW' = 'HIGH'; + let reason = `Version ${latestVersion} available. `; + + if (hasBreakingChanges) { + confidence = 'MEDIUM'; + reason += 'Contains breaking changes. Review before upgrading.'; + } else { + reason += 'Safe to upgrade (no breaking changes detected).'; + } + + if (versionGap > 2) { + confidence = 'LOW'; + reason += ` Version gap is large (${versionGap} versions). Consider incremental upgrade.`; + } + + return { + nodeType, + currentVersion, + latestVersion, + isOutdated, + versionGap, + hasBreakingChanges, + recommendUpgrade, + confidence, + reason + }; + } + + /** + * Calculate the version gap (number of versions between) + */ + private calculateVersionGap(fromVersion: string, toVersion: string): number { + const from = fromVersion.split('.').map(Number); + const to = toVersion.split('.').map(Number); + + // Simple gap calculation based on version numbers + let gap = 0; + + for (let i = 0; i < Math.max(from.length, to.length); i++) { + const f = from[i] || 0; + const t = to[i] || 0; + gap += Math.abs(t - f); + } + + return gap; + } + + /** + * Suggest the best upgrade path for a node + */ + async suggestUpgradePath(nodeType: string, currentVersion: string): Promise { + const latestVersion = this.getLatestVersion(nodeType); + + if (!latestVersion) return null; + + const comparison = this.compareVersions(currentVersion, latestVersion); + if (comparison >= 0) return null; // Already at latest or newer + + // Get all available versions between current and latest + const allVersions = this.getAvailableVersions(nodeType); + const intermediateVersions = allVersions + .filter(v => + this.compareVersions(v.version, currentVersion) > 0 && + this.compareVersions(v.version, latestVersion) < 0 + ) + .map(v => v.version) + .sort((a, b) => this.compareVersions(a, b)); + + // Analyze the upgrade + const analysis = await this.breakingChangeDetector.analyzeVersionUpgrade( + nodeType, + currentVersion, + latestVersion + ); + + // Determine if direct upgrade is safe + const versionGap = this.calculateVersionGap(currentVersion, latestVersion); + const direct = versionGap <= 1 || !analysis.hasBreakingChanges; + + // Generate upgrade steps + const steps: UpgradeStep[] = []; + + if (direct || intermediateVersions.length === 0) { + // Direct upgrade + steps.push({ + fromVersion: currentVersion, + toVersion: latestVersion, + breakingChanges: analysis.changes.filter(c => c.isBreaking).length, + migrationHints: analysis.recommendations + }); + } else { + // Multi-step upgrade through intermediate versions + let stepFrom = currentVersion; + + for (const intermediateVersion of intermediateVersions) { + const stepAnalysis = await this.breakingChangeDetector.analyzeVersionUpgrade( + nodeType, + stepFrom, + intermediateVersion + ); + + steps.push({ + fromVersion: stepFrom, + toVersion: intermediateVersion, + breakingChanges: stepAnalysis.changes.filter(c => c.isBreaking).length, + migrationHints: stepAnalysis.recommendations + }); + + stepFrom = intermediateVersion; + } + + // Final step to latest + const finalStepAnalysis = await this.breakingChangeDetector.analyzeVersionUpgrade( + nodeType, + stepFrom, + latestVersion + ); + + steps.push({ + fromVersion: stepFrom, + toVersion: latestVersion, + breakingChanges: finalStepAnalysis.changes.filter(c => c.isBreaking).length, + migrationHints: finalStepAnalysis.recommendations + }); + } + + // Calculate estimated effort + const totalBreakingChanges = steps.reduce((sum, step) => sum + step.breakingChanges, 0); + let estimatedEffort: 'LOW' | 'MEDIUM' | 'HIGH' = 'LOW'; + + if (totalBreakingChanges > 5 || steps.length > 3) { + estimatedEffort = 'HIGH'; + } else if (totalBreakingChanges > 2 || steps.length > 1) { + estimatedEffort = 'MEDIUM'; + } + + return { + nodeType, + fromVersion: currentVersion, + toVersion: latestVersion, + direct, + intermediateVersions, + totalBreakingChanges, + autoMigratableChanges: analysis.autoMigratableCount, + manualRequiredChanges: analysis.manualRequiredCount, + estimatedEffort, + steps + }; + } + + /** + * Check if a specific version exists for a node + */ + versionExists(nodeType: string, version: string): boolean { + const versions = this.getAvailableVersions(nodeType); + return versions.some(v => v.version === version); + } + + /** + * Get version metadata (breaking changes, added/deprecated properties) + */ + getVersionMetadata(nodeType: string, version: string): NodeVersion | null { + const versionData = this.nodeRepository.getNodeVersion(nodeType, version); + return versionData; + } + + /** + * Clear the version cache + */ + clearCache(nodeType?: string): void { + if (nodeType) { + this.versionCache.delete(nodeType); + this.cacheTimestamps.delete(nodeType); + } else { + this.versionCache.clear(); + this.cacheTimestamps.clear(); + } + } + + /** + * Get cached versions if still valid + */ + private getCachedVersions(nodeType: string): NodeVersion[] | null { + const cached = this.versionCache.get(nodeType); + const timestamp = this.cacheTimestamps.get(nodeType); + + if (cached && timestamp) { + const age = Date.now() - timestamp; + if (age < this.cacheTTL) { + return cached; + } + } + + return null; + } + + /** + * Cache versions with timestamp + */ + private cacheVersions(nodeType: string, versions: NodeVersion[]): void { + this.versionCache.set(nodeType, versions); + this.cacheTimestamps.set(nodeType, Date.now()); + } +} diff --git a/src/services/post-update-validator.ts b/src/services/post-update-validator.ts new file mode 100644 index 0000000..926a991 --- /dev/null +++ b/src/services/post-update-validator.ts @@ -0,0 +1,423 @@ +/** + * Post-Update Validator + * + * Generates comprehensive, AI-friendly migration reports after node version upgrades. + * Provides actionable guidance for AI agents on what manual steps are needed. + * + * Validation includes: + * - New required properties + * - Deprecated/removed properties + * - Behavior changes + * - Step-by-step migration instructions + */ + +import { BreakingChangeDetector, DetectedChange } from './breaking-change-detector'; +import { MigrationResult } from './node-migration-service'; +import { NodeVersionService } from './node-version-service'; + +export interface PostUpdateGuidance { + nodeId: string; + nodeName: string; + nodeType: string; + oldVersion: string; + newVersion: string; + migrationStatus: 'complete' | 'partial' | 'manual_required'; + requiredActions: RequiredAction[]; + deprecatedProperties: DeprecatedProperty[]; + behaviorChanges: BehaviorChange[]; + migrationSteps: string[]; + confidence: 'HIGH' | 'MEDIUM' | 'LOW'; + estimatedTime: string; // e.g., "5 minutes", "15 minutes" +} + +export interface RequiredAction { + type: 'ADD_PROPERTY' | 'UPDATE_PROPERTY' | 'CONFIGURE_OPTION' | 'REVIEW_CONFIGURATION'; + property: string; + reason: string; + suggestedValue?: any; + currentValue?: any; + documentation?: string; + priority: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; +} + +export interface DeprecatedProperty { + property: string; + status: 'removed' | 'deprecated'; + replacement?: string; + action: 'remove' | 'replace' | 'ignore'; + impact: 'breaking' | 'warning'; +} + +export interface BehaviorChange { + aspect: string; // e.g., "data passing", "webhook handling" + oldBehavior: string; + newBehavior: string; + impact: 'HIGH' | 'MEDIUM' | 'LOW'; + actionRequired: boolean; + recommendation: string; +} + +export class PostUpdateValidator { + constructor( + private versionService: NodeVersionService, + private breakingChangeDetector: BreakingChangeDetector + ) {} + + /** + * Generate comprehensive post-update guidance for a migrated node + */ + async generateGuidance( + nodeId: string, + nodeName: string, + nodeType: string, + oldVersion: string, + newVersion: string, + migrationResult: MigrationResult + ): Promise { + // Analyze the version upgrade + const analysis = await this.breakingChangeDetector.analyzeVersionUpgrade( + nodeType, + oldVersion, + newVersion + ); + + // Determine migration status + const migrationStatus = this.determineMigrationStatus(migrationResult, analysis.changes); + + // Generate required actions + const requiredActions = this.generateRequiredActions( + migrationResult, + analysis.changes, + nodeType + ); + + // Identify deprecated properties + const deprecatedProperties = this.identifyDeprecatedProperties(analysis.changes); + + // Document behavior changes + const behaviorChanges = this.documentBehaviorChanges(nodeType, oldVersion, newVersion); + + // Generate step-by-step migration instructions + const migrationSteps = this.generateMigrationSteps( + requiredActions, + deprecatedProperties, + behaviorChanges + ); + + // Calculate confidence and estimated time + const confidence = this.calculateConfidence(requiredActions, migrationStatus); + const estimatedTime = this.estimateTime(requiredActions, behaviorChanges); + + return { + nodeId, + nodeName, + nodeType, + oldVersion, + newVersion, + migrationStatus, + requiredActions, + deprecatedProperties, + behaviorChanges, + migrationSteps, + confidence, + estimatedTime + }; + } + + /** + * Determine the migration status based on results and changes + */ + private determineMigrationStatus( + migrationResult: MigrationResult, + changes: DetectedChange[] + ): 'complete' | 'partial' | 'manual_required' { + if (migrationResult.remainingIssues.length === 0) { + return 'complete'; + } + + const criticalIssues = changes.filter(c => c.isBreaking && !c.autoMigratable); + + if (criticalIssues.length > 0) { + return 'manual_required'; + } + + return 'partial'; + } + + /** + * Generate actionable required actions for the AI agent + */ + private generateRequiredActions( + migrationResult: MigrationResult, + changes: DetectedChange[], + nodeType: string + ): RequiredAction[] { + const actions: RequiredAction[] = []; + + // Actions from remaining issues (not auto-migrated) + const manualChanges = changes.filter(c => !c.autoMigratable); + + for (const change of manualChanges) { + actions.push({ + type: this.mapChangeTypeToActionType(change.changeType), + property: change.propertyName, + reason: change.migrationHint, + suggestedValue: change.newValue, + currentValue: change.oldValue, + documentation: this.getPropertyDocumentation(nodeType, change.propertyName), + priority: this.mapSeverityToPriority(change.severity) + }); + } + + return actions; + } + + /** + * Identify deprecated or removed properties + */ + private identifyDeprecatedProperties(changes: DetectedChange[]): DeprecatedProperty[] { + const deprecated: DeprecatedProperty[] = []; + + for (const change of changes) { + if (change.changeType === 'removed') { + deprecated.push({ + property: change.propertyName, + status: 'removed', + replacement: change.migrationStrategy?.targetProperty, + action: change.autoMigratable ? 'remove' : 'replace', + impact: change.isBreaking ? 'breaking' : 'warning' + }); + } + } + + return deprecated; + } + + /** + * Document behavior changes for specific nodes + */ + private documentBehaviorChanges( + nodeType: string, + oldVersion: string, + newVersion: string + ): BehaviorChange[] { + const changes: BehaviorChange[] = []; + + // Execute Workflow node behavior changes + if (nodeType === 'n8n-nodes-base.executeWorkflow') { + if (this.versionService.compareVersions(oldVersion, '1.1') < 0 && + this.versionService.compareVersions(newVersion, '1.1') >= 0) { + changes.push({ + aspect: 'Data passing to sub-workflows', + oldBehavior: 'Automatic data passing - all data from parent workflow automatically available', + newBehavior: 'Explicit field mapping required - must define inputFieldMapping to pass specific fields', + impact: 'HIGH', + actionRequired: true, + recommendation: 'Define inputFieldMapping with specific field mappings between parent and child workflows. Review data dependencies.' + }); + } + } + + // Webhook node behavior changes + if (nodeType === 'n8n-nodes-base.webhook') { + if (this.versionService.compareVersions(oldVersion, '2.1') < 0 && + this.versionService.compareVersions(newVersion, '2.1') >= 0) { + changes.push({ + aspect: 'Webhook persistence', + oldBehavior: 'Webhook URL changes on workflow updates', + newBehavior: 'Stable webhook URL via webhookId field', + impact: 'MEDIUM', + actionRequired: false, + recommendation: 'Webhook URLs now remain stable across workflow updates. Update external systems if needed.' + }); + } + + if (this.versionService.compareVersions(oldVersion, '2.0') < 0 && + this.versionService.compareVersions(newVersion, '2.0') >= 0) { + changes.push({ + aspect: 'Response handling', + oldBehavior: 'Automatic response after webhook trigger', + newBehavior: 'Configurable response mode (onReceived vs lastNode)', + impact: 'MEDIUM', + actionRequired: true, + recommendation: 'Review responseMode setting. Use "onReceived" for immediate responses or "lastNode" to wait for workflow completion.' + }); + } + } + + return changes; + } + + /** + * Generate step-by-step migration instructions for AI agents + */ + private generateMigrationSteps( + requiredActions: RequiredAction[], + deprecatedProperties: DeprecatedProperty[], + behaviorChanges: BehaviorChange[] + ): string[] { + const steps: string[] = []; + let stepNumber = 1; + + // Start with deprecations + if (deprecatedProperties.length > 0) { + steps.push(`Step ${stepNumber++}: Remove deprecated properties`); + for (const dep of deprecatedProperties) { + steps.push(` - Remove "${dep.property}" ${dep.replacement ? `(use "${dep.replacement}" instead)` : ''}`); + } + } + + // Then critical actions + const criticalActions = requiredActions.filter(a => a.priority === 'CRITICAL'); + if (criticalActions.length > 0) { + steps.push(`Step ${stepNumber++}: Address critical configuration requirements`); + for (const action of criticalActions) { + steps.push(` - ${action.property}: ${action.reason}`); + if (action.suggestedValue !== undefined) { + steps.push(` Suggested value: ${JSON.stringify(action.suggestedValue)}`); + } + } + } + + // High priority actions + const highActions = requiredActions.filter(a => a.priority === 'HIGH'); + if (highActions.length > 0) { + steps.push(`Step ${stepNumber++}: Configure required properties`); + for (const action of highActions) { + steps.push(` - ${action.property}: ${action.reason}`); + } + } + + // Behavior change adaptations + const actionRequiredChanges = behaviorChanges.filter(c => c.actionRequired); + if (actionRequiredChanges.length > 0) { + steps.push(`Step ${stepNumber++}: Adapt to behavior changes`); + for (const change of actionRequiredChanges) { + steps.push(` - ${change.aspect}: ${change.recommendation}`); + } + } + + // Medium/Low priority actions + const otherActions = requiredActions.filter(a => a.priority === 'MEDIUM' || a.priority === 'LOW'); + if (otherActions.length > 0) { + steps.push(`Step ${stepNumber++}: Review optional configurations`); + for (const action of otherActions) { + steps.push(` - ${action.property}: ${action.reason}`); + } + } + + // Final validation step + steps.push(`Step ${stepNumber}: Test workflow execution`); + steps.push(' - Validate all node configurations'); + steps.push(' - Run a test execution'); + steps.push(' - Verify expected behavior'); + + return steps; + } + + /** + * Map change type to action type + */ + private mapChangeTypeToActionType( + changeType: string + ): 'ADD_PROPERTY' | 'UPDATE_PROPERTY' | 'CONFIGURE_OPTION' | 'REVIEW_CONFIGURATION' { + switch (changeType) { + case 'added': + return 'ADD_PROPERTY'; + case 'requirement_changed': + case 'type_changed': + return 'UPDATE_PROPERTY'; + case 'default_changed': + return 'CONFIGURE_OPTION'; + default: + return 'REVIEW_CONFIGURATION'; + } + } + + /** + * Map severity to priority + */ + private mapSeverityToPriority( + severity: 'LOW' | 'MEDIUM' | 'HIGH' + ): 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' { + if (severity === 'HIGH') return 'CRITICAL'; + return severity; + } + + /** + * Get documentation for a property (placeholder - would integrate with node docs) + */ + private getPropertyDocumentation(nodeType: string, propertyName: string): string { + // In future, this would fetch from node documentation + return `See n8n documentation for ${nodeType} - ${propertyName}`; + } + + /** + * Calculate overall confidence in the migration + */ + private calculateConfidence( + requiredActions: RequiredAction[], + migrationStatus: 'complete' | 'partial' | 'manual_required' + ): 'HIGH' | 'MEDIUM' | 'LOW' { + if (migrationStatus === 'complete') return 'HIGH'; + + const criticalActions = requiredActions.filter(a => a.priority === 'CRITICAL'); + + if (migrationStatus === 'manual_required' || criticalActions.length > 3) { + return 'LOW'; + } + + return 'MEDIUM'; + } + + /** + * Estimate time required for manual migration steps + */ + private estimateTime( + requiredActions: RequiredAction[], + behaviorChanges: BehaviorChange[] + ): string { + const criticalCount = requiredActions.filter(a => a.priority === 'CRITICAL').length; + const highCount = requiredActions.filter(a => a.priority === 'HIGH').length; + const behaviorCount = behaviorChanges.filter(c => c.actionRequired).length; + + const totalComplexity = criticalCount * 5 + highCount * 3 + behaviorCount * 2; + + if (totalComplexity === 0) return '< 1 minute'; + if (totalComplexity <= 5) return '2-5 minutes'; + if (totalComplexity <= 10) return '5-10 minutes'; + if (totalComplexity <= 20) return '10-20 minutes'; + return '20+ minutes'; + } + + /** + * Generate a human-readable summary for logging/display + */ + generateSummary(guidance: PostUpdateGuidance): string { + const lines: string[] = []; + + lines.push(`Node "${guidance.nodeName}" upgraded from v${guidance.oldVersion} to v${guidance.newVersion}`); + lines.push(`Status: ${guidance.migrationStatus.toUpperCase()}`); + lines.push(`Confidence: ${guidance.confidence}`); + lines.push(`Estimated time: ${guidance.estimatedTime}`); + + if (guidance.requiredActions.length > 0) { + lines.push(`\nRequired actions: ${guidance.requiredActions.length}`); + for (const action of guidance.requiredActions.slice(0, 3)) { + lines.push(` - [${action.priority}] ${action.property}: ${action.reason}`); + } + if (guidance.requiredActions.length > 3) { + lines.push(` ... and ${guidance.requiredActions.length - 3} more`); + } + } + + if (guidance.behaviorChanges.length > 0) { + lines.push(`\nBehavior changes: ${guidance.behaviorChanges.length}`); + for (const change of guidance.behaviorChanges) { + lines.push(` - ${change.aspect}: ${change.newBehavior}`); + } + } + + return lines.join('\n'); + } +} diff --git a/src/services/workflow-auto-fixer.ts b/src/services/workflow-auto-fixer.ts index 27b4fe0..32c4645 100644 --- a/src/services/workflow-auto-fixer.ts +++ b/src/services/workflow-auto-fixer.ts @@ -16,6 +16,10 @@ import { } from '../types/workflow-diff'; import { WorkflowNode, Workflow } from '../types/n8n-api'; import { Logger } from '../utils/logger'; +import { NodeVersionService } from './node-version-service'; +import { BreakingChangeDetector } from './breaking-change-detector'; +import { NodeMigrationService } from './node-migration-service'; +import { PostUpdateValidator, PostUpdateGuidance } from './post-update-validator'; const logger = new Logger({ prefix: '[WorkflowAutoFixer]' }); @@ -25,7 +29,9 @@ export type FixType = | 'typeversion-correction' | 'error-output-config' | 'node-type-correction' - | 'webhook-missing-path'; + | 'webhook-missing-path' + | 'typeversion-upgrade' // NEW: Proactive version upgrades + | 'version-migration'; // NEW: Smart version migrations with breaking changes export interface AutoFixConfig { applyFixes: boolean; @@ -53,6 +59,7 @@ export interface AutoFixResult { byType: Record; byConfidence: Record; }; + postUpdateGuidance?: PostUpdateGuidance[]; // NEW: AI-friendly migration guidance } export interface NodeFormatIssue extends ExpressionFormatIssue { @@ -91,25 +98,34 @@ export class WorkflowAutoFixer { maxFixes: 50 }; private similarityService: NodeSimilarityService | null = null; + private versionService: NodeVersionService | null = null; + private breakingChangeDetector: BreakingChangeDetector | null = null; + private migrationService: NodeMigrationService | null = null; + private postUpdateValidator: PostUpdateValidator | null = null; constructor(repository?: NodeRepository) { if (repository) { this.similarityService = new NodeSimilarityService(repository); + this.breakingChangeDetector = new BreakingChangeDetector(repository); + this.versionService = new NodeVersionService(repository, this.breakingChangeDetector); + this.migrationService = new NodeMigrationService(this.versionService, this.breakingChangeDetector); + this.postUpdateValidator = new PostUpdateValidator(this.versionService, this.breakingChangeDetector); } } /** * Generate fix operations from validation results */ - generateFixes( + async generateFixes( workflow: Workflow, validationResult: WorkflowValidationResult, formatIssues: ExpressionFormatIssue[] = [], config: Partial = {} - ): AutoFixResult { + ): Promise { const fullConfig = { ...this.defaultConfig, ...config }; const operations: WorkflowDiffOperation[] = []; const fixes: FixOperation[] = []; + const postUpdateGuidance: PostUpdateGuidance[] = []; // Create a map for quick node lookup const nodeMap = new Map(); @@ -143,6 +159,16 @@ export class WorkflowAutoFixer { this.processWebhookPathFixes(validationResult, nodeMap, operations, fixes); } + // NEW: Process version upgrades (HIGH/MEDIUM confidence) + if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('typeversion-upgrade')) { + await this.processVersionUpgradeFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance); + } + + // NEW: Process version migrations with breaking changes (MEDIUM/LOW confidence) + if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('version-migration')) { + await this.processVersionMigrationFixes(workflow, nodeMap, operations, fixes, postUpdateGuidance); + } + // Filter by confidence threshold const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold); const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes); @@ -159,7 +185,8 @@ export class WorkflowAutoFixer { operations: limitedOperations, fixes: limitedFixes, summary, - stats + stats, + postUpdateGuidance: postUpdateGuidance.length > 0 ? postUpdateGuidance : undefined }; } @@ -578,7 +605,9 @@ export class WorkflowAutoFixer { 'typeversion-correction': 0, 'error-output-config': 0, 'node-type-correction': 0, - 'webhook-missing-path': 0 + 'webhook-missing-path': 0, + 'typeversion-upgrade': 0, + 'version-migration': 0 }, byConfidence: { 'high': 0, @@ -621,10 +650,186 @@ export class WorkflowAutoFixer { parts.push(`${stats.byType['webhook-missing-path']} webhook ${stats.byType['webhook-missing-path'] === 1 ? 'path' : 'paths'}`); } + if (stats.byType['typeversion-upgrade'] > 0) { + parts.push(`${stats.byType['typeversion-upgrade']} version ${stats.byType['typeversion-upgrade'] === 1 ? 'upgrade' : 'upgrades'}`); + } + if (stats.byType['version-migration'] > 0) { + parts.push(`${stats.byType['version-migration']} version ${stats.byType['version-migration'] === 1 ? 'migration' : 'migrations'}`); + } + if (parts.length === 0) { return `Fixed ${stats.total} ${stats.total === 1 ? 'issue' : 'issues'}`; } return `Fixed ${parts.join(', ')}`; } + + /** + * Process version upgrade fixes (proactive upgrades to latest versions) + * HIGH confidence for non-breaking upgrades, MEDIUM for upgrades with auto-migratable changes + */ + private async processVersionUpgradeFixes( + workflow: Workflow, + nodeMap: Map, + operations: WorkflowDiffOperation[], + fixes: FixOperation[], + postUpdateGuidance: PostUpdateGuidance[] + ): Promise { + if (!this.versionService || !this.migrationService || !this.postUpdateValidator) { + logger.warn('Version services not initialized. Skipping version upgrade fixes.'); + return; + } + + for (const node of workflow.nodes) { + if (!node.typeVersion || !node.type) continue; + + const currentVersion = node.typeVersion.toString(); + const analysis = this.versionService.analyzeVersion(node.type, currentVersion); + + // Only upgrade if outdated and recommended + if (!analysis.isOutdated || !analysis.recommendUpgrade) continue; + + // Skip if confidence is too low + if (analysis.confidence === 'LOW') continue; + + const latestVersion = analysis.latestVersion; + + // Attempt migration + try { + const migrationResult = await this.migrationService.migrateNode( + node, + currentVersion, + latestVersion + ); + + // Create fix operation + fixes.push({ + node: node.name, + field: 'typeVersion', + type: 'typeversion-upgrade', + before: currentVersion, + after: latestVersion, + confidence: analysis.hasBreakingChanges ? 'medium' : 'high', + description: `Upgrade ${node.name} from v${currentVersion} to v${latestVersion}. ${analysis.reason}` + }); + + // Create update operation + const operation: UpdateNodeOperation = { + type: 'updateNode', + nodeId: node.id, + updates: { + typeVersion: parseFloat(latestVersion), + parameters: migrationResult.updatedNode.parameters, + ...(migrationResult.updatedNode.webhookId && { webhookId: migrationResult.updatedNode.webhookId }) + } + }; + operations.push(operation); + + // Generate post-update guidance + const guidance = await this.postUpdateValidator.generateGuidance( + node.id, + node.name, + node.type, + currentVersion, + latestVersion, + migrationResult + ); + + postUpdateGuidance.push(guidance); + + logger.info(`Generated version upgrade fix for ${node.name}: ${currentVersion} → ${latestVersion}`, { + appliedMigrations: migrationResult.appliedMigrations.length, + remainingIssues: migrationResult.remainingIssues.length + }); + } catch (error) { + logger.error(`Failed to process version upgrade for ${node.name}`, { error }); + } + } + } + + /** + * Process version migration fixes (handle breaking changes with smart migrations) + * MEDIUM/LOW confidence for migrations requiring manual intervention + */ + private async processVersionMigrationFixes( + workflow: Workflow, + nodeMap: Map, + operations: WorkflowDiffOperation[], + fixes: FixOperation[], + postUpdateGuidance: PostUpdateGuidance[] + ): Promise { + // This method handles migrations that weren't covered by typeversion-upgrade + // Focuses on nodes with complex breaking changes that need manual review + + if (!this.versionService || !this.breakingChangeDetector || !this.postUpdateValidator) { + logger.warn('Version services not initialized. Skipping version migration fixes.'); + return; + } + + for (const node of workflow.nodes) { + if (!node.typeVersion || !node.type) continue; + + const currentVersion = node.typeVersion.toString(); + const latestVersion = this.versionService.getLatestVersion(node.type); + + if (!latestVersion || currentVersion === latestVersion) continue; + + // Check if this has breaking changes + const hasBreaking = this.breakingChangeDetector.hasBreakingChanges( + node.type, + currentVersion, + latestVersion + ); + + if (!hasBreaking) continue; // Already handled by typeversion-upgrade + + // Analyze the migration + const analysis = await this.breakingChangeDetector.analyzeVersionUpgrade( + node.type, + currentVersion, + latestVersion + ); + + // Only proceed if there are non-auto-migratable changes + if (analysis.autoMigratableCount === analysis.changes.length) continue; + + // Generate guidance for manual migration + const guidance = await this.postUpdateValidator.generateGuidance( + node.id, + node.name, + node.type, + currentVersion, + latestVersion, + { + success: false, + nodeId: node.id, + nodeName: node.name, + fromVersion: currentVersion, + toVersion: latestVersion, + appliedMigrations: [], + remainingIssues: analysis.recommendations, + confidence: analysis.overallSeverity === 'HIGH' ? 'LOW' : 'MEDIUM', + updatedNode: node + } + ); + + // Create a fix entry (won't be auto-applied, just documented) + fixes.push({ + node: node.name, + field: 'typeVersion', + type: 'version-migration', + before: currentVersion, + after: latestVersion, + confidence: guidance.confidence === 'HIGH' ? 'medium' : 'low', + description: `Version migration required: ${node.name} v${currentVersion} → v${latestVersion}. ${analysis.manualRequiredCount} manual action(s) required.` + }); + + postUpdateGuidance.push(guidance); + + logger.info(`Documented version migration for ${node.name}`, { + breakingChanges: analysis.changes.filter(c => c.isBreaking).length, + manualRequired: analysis.manualRequiredCount + }); + } + } } \ No newline at end of file