@sparkleideas/testing 3.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +547 -0
- package/__tests__/framework.test.ts +21 -0
- package/package.json +61 -0
- package/src/fixtures/agent-fixtures.ts +793 -0
- package/src/fixtures/agents.ts +212 -0
- package/src/fixtures/configurations.ts +491 -0
- package/src/fixtures/index.ts +21 -0
- package/src/fixtures/mcp-fixtures.ts +1030 -0
- package/src/fixtures/memory-entries.ts +328 -0
- package/src/fixtures/memory-fixtures.ts +750 -0
- package/src/fixtures/swarm-fixtures.ts +837 -0
- package/src/fixtures/tasks.ts +309 -0
- package/src/helpers/assertion-helpers.ts +616 -0
- package/src/helpers/assertions.ts +286 -0
- package/src/helpers/create-mock.ts +200 -0
- package/src/helpers/index.ts +182 -0
- package/src/helpers/mock-factory.ts +711 -0
- package/src/helpers/setup-teardown.ts +678 -0
- package/src/helpers/swarm-instance.ts +326 -0
- package/src/helpers/test-application.ts +310 -0
- package/src/helpers/test-utils.ts +670 -0
- package/src/index.ts +232 -0
- package/src/mocks/index.ts +29 -0
- package/src/mocks/mock-mcp-client.ts +723 -0
- package/src/mocks/mock-services.ts +793 -0
- package/src/regression/api-contract.ts +473 -0
- package/src/regression/index.ts +46 -0
- package/src/regression/integration-regression.ts +416 -0
- package/src/regression/performance-baseline.ts +356 -0
- package/src/regression/regression-runner.ts +339 -0
- package/src/regression/security-regression.ts +331 -0
- package/src/setup.ts +127 -0
- package/src/v2-compat/api-compat.test.ts +590 -0
- package/src/v2-compat/cli-compat.test.ts +484 -0
- package/src/v2-compat/compatibility-validator.ts +1072 -0
- package/src/v2-compat/hooks-compat.test.ts +602 -0
- package/src/v2-compat/index.ts +58 -0
- package/src/v2-compat/mcp-compat.test.ts +557 -0
- package/src/v2-compat/report-generator.ts +441 -0
- package/tmp.json +0 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 Compatibility Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates that V3 implementation maintains backward compatibility with V2 capabilities.
|
|
5
|
+
* Tests CLI commands, MCP tools, hooks, and API interfaces.
|
|
6
|
+
*
|
|
7
|
+
* @module v3/testing/v2-compat/compatibility-validator
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { vi } from 'vitest';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validation result for a single check
|
|
14
|
+
*/
|
|
15
|
+
export interface ValidationCheck {
|
|
16
|
+
name: string;
|
|
17
|
+
category: 'cli' | 'mcp' | 'hooks' | 'api';
|
|
18
|
+
passed: boolean;
|
|
19
|
+
message: string;
|
|
20
|
+
v2Behavior: string;
|
|
21
|
+
v3Behavior: string;
|
|
22
|
+
breaking: boolean;
|
|
23
|
+
migrationPath?: string;
|
|
24
|
+
details?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validation result for a category
|
|
29
|
+
*/
|
|
30
|
+
export interface ValidationResult {
|
|
31
|
+
category: 'cli' | 'mcp' | 'hooks' | 'api';
|
|
32
|
+
totalChecks: number;
|
|
33
|
+
passedChecks: number;
|
|
34
|
+
failedChecks: number;
|
|
35
|
+
breakingChanges: number;
|
|
36
|
+
checks: ValidationCheck[];
|
|
37
|
+
duration: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Full validation report
|
|
42
|
+
*/
|
|
43
|
+
export interface FullValidationReport {
|
|
44
|
+
timestamp: Date;
|
|
45
|
+
v2Version: string;
|
|
46
|
+
v3Version: string;
|
|
47
|
+
overallPassed: boolean;
|
|
48
|
+
totalChecks: number;
|
|
49
|
+
passedChecks: number;
|
|
50
|
+
failedChecks: number;
|
|
51
|
+
breakingChanges: number;
|
|
52
|
+
cli: ValidationResult;
|
|
53
|
+
mcp: ValidationResult;
|
|
54
|
+
hooks: ValidationResult;
|
|
55
|
+
api: ValidationResult;
|
|
56
|
+
summary: string;
|
|
57
|
+
recommendations: string[];
|
|
58
|
+
duration: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* V2 CLI command definition
|
|
63
|
+
*/
|
|
64
|
+
export interface V2CLICommand {
|
|
65
|
+
name: string;
|
|
66
|
+
aliases: string[];
|
|
67
|
+
flags: string[];
|
|
68
|
+
description: string;
|
|
69
|
+
v3Equivalent?: string;
|
|
70
|
+
deprecated?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* V2 MCP tool definition
|
|
75
|
+
*/
|
|
76
|
+
export interface V2MCPTool {
|
|
77
|
+
name: string;
|
|
78
|
+
parameters: Record<string, { type: string; required: boolean }>;
|
|
79
|
+
returnType: string;
|
|
80
|
+
v3Equivalent?: string;
|
|
81
|
+
deprecated?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* V2 hook definition
|
|
86
|
+
*/
|
|
87
|
+
export interface V2Hook {
|
|
88
|
+
name: string;
|
|
89
|
+
trigger: string;
|
|
90
|
+
parameters: string[];
|
|
91
|
+
returnType: string;
|
|
92
|
+
v3Equivalent?: string;
|
|
93
|
+
deprecated?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* V2 API interface definition
|
|
98
|
+
*/
|
|
99
|
+
export interface V2APIInterface {
|
|
100
|
+
name: string;
|
|
101
|
+
methods: { name: string; signature: string }[];
|
|
102
|
+
v3Equivalent?: string;
|
|
103
|
+
deprecated?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* V2 CLI Commands (25 total)
|
|
108
|
+
*/
|
|
109
|
+
export const V2_CLI_COMMANDS: V2CLICommand[] = [
|
|
110
|
+
// Core commands
|
|
111
|
+
{ name: 'init', aliases: ['i'], flags: ['--force', '--template'], description: 'Initialize claude-flow project', v3Equivalent: 'init' },
|
|
112
|
+
{ name: 'start', aliases: ['s'], flags: ['--detached', '--port'], description: 'Start MCP server', v3Equivalent: 'start' },
|
|
113
|
+
{ name: 'stop', aliases: [], flags: ['--force'], description: 'Stop MCP server', v3Equivalent: 'stop' },
|
|
114
|
+
{ name: 'status', aliases: ['st'], flags: ['--json', '--verbose'], description: 'Show system status', v3Equivalent: 'status' },
|
|
115
|
+
{ name: 'config', aliases: ['c'], flags: ['--get', '--set', '--list'], description: 'Manage configuration', v3Equivalent: 'config' },
|
|
116
|
+
|
|
117
|
+
// Agent commands
|
|
118
|
+
{ name: 'agent spawn', aliases: ['a spawn'], flags: ['--type', '--id', '--config'], description: 'Spawn new agent', v3Equivalent: 'agent spawn' },
|
|
119
|
+
{ name: 'agent list', aliases: ['a ls'], flags: ['--status', '--type'], description: 'List agents', v3Equivalent: 'agent list' },
|
|
120
|
+
{ name: 'agent terminate', aliases: ['a kill'], flags: ['--force', '--all'], description: 'Terminate agent', v3Equivalent: 'agent terminate' },
|
|
121
|
+
{ name: 'agent info', aliases: ['a info'], flags: ['--metrics'], description: 'Show agent info', v3Equivalent: 'agent status' },
|
|
122
|
+
|
|
123
|
+
// Swarm commands
|
|
124
|
+
{ name: 'swarm init', aliases: ['sw init'], flags: ['--topology', '--max-agents'], description: 'Initialize swarm', v3Equivalent: 'swarm init' },
|
|
125
|
+
{ name: 'swarm status', aliases: ['sw st'], flags: ['--detailed'], description: 'Show swarm status', v3Equivalent: 'swarm status' },
|
|
126
|
+
{ name: 'swarm scale', aliases: ['sw scale'], flags: ['--up', '--down'], description: 'Scale swarm', v3Equivalent: 'swarm scale' },
|
|
127
|
+
|
|
128
|
+
// Memory commands
|
|
129
|
+
{ name: 'memory list', aliases: ['mem ls'], flags: ['--type', '--limit'], description: 'List memories', v3Equivalent: 'memory list' },
|
|
130
|
+
{ name: 'memory query', aliases: ['mem q'], flags: ['--search', '--type'], description: 'Query memory', v3Equivalent: 'memory search' },
|
|
131
|
+
{ name: 'memory clear', aliases: ['mem clear'], flags: ['--force', '--type'], description: 'Clear memory', v3Equivalent: 'memory clear' },
|
|
132
|
+
|
|
133
|
+
// Hooks commands
|
|
134
|
+
{ name: 'hooks pre-edit', aliases: [], flags: ['--file'], description: 'Pre-edit hook', v3Equivalent: 'hooks pre-edit' },
|
|
135
|
+
{ name: 'hooks post-edit', aliases: [], flags: ['--file', '--success'], description: 'Post-edit hook', v3Equivalent: 'hooks post-edit' },
|
|
136
|
+
{ name: 'hooks pre-command', aliases: [], flags: ['--command'], description: 'Pre-command hook', v3Equivalent: 'hooks pre-command' },
|
|
137
|
+
{ name: 'hooks post-command', aliases: [], flags: ['--command', '--success'], description: 'Post-command hook', v3Equivalent: 'hooks post-command' },
|
|
138
|
+
{ name: 'hooks route', aliases: [], flags: ['--task'], description: 'Route task', v3Equivalent: 'hooks route' },
|
|
139
|
+
{ name: 'hooks pretrain', aliases: [], flags: [], description: 'Pretrain from repo', v3Equivalent: 'hooks pretrain' },
|
|
140
|
+
{ name: 'hooks metrics', aliases: [], flags: ['--dashboard'], description: 'Show metrics', v3Equivalent: 'hooks metrics' },
|
|
141
|
+
|
|
142
|
+
// Deprecated but supported
|
|
143
|
+
{ name: 'hive-mind init', aliases: [], flags: [], description: 'Initialize hive', v3Equivalent: 'swarm init', deprecated: true },
|
|
144
|
+
{ name: 'neural init', aliases: [], flags: [], description: 'Initialize neural', v3Equivalent: 'hooks pretrain', deprecated: true },
|
|
145
|
+
{ name: 'goal init', aliases: [], flags: [], description: 'Initialize goals', v3Equivalent: 'hooks pretrain', deprecated: true },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* V2 MCP Tools (65 total - showing key ones)
|
|
150
|
+
*/
|
|
151
|
+
export const V2_MCP_TOOLS: V2MCPTool[] = [
|
|
152
|
+
// Agent tools
|
|
153
|
+
{ name: 'dispatch_agent', parameters: { type: { type: 'string', required: true }, name: { type: 'string', required: false } }, returnType: 'AgentInfo', v3Equivalent: 'agent/spawn' },
|
|
154
|
+
{ name: 'agents/spawn', parameters: { type: { type: 'string', required: true }, config: { type: 'object', required: false } }, returnType: 'AgentInfo', v3Equivalent: 'agent/spawn' },
|
|
155
|
+
{ name: 'agents/list', parameters: { status: { type: 'string', required: false } }, returnType: 'AgentInfo[]', v3Equivalent: 'agent/list' },
|
|
156
|
+
{ name: 'agents/terminate', parameters: { id: { type: 'string', required: true } }, returnType: 'boolean', v3Equivalent: 'agent/terminate' },
|
|
157
|
+
{ name: 'agents/info', parameters: { id: { type: 'string', required: true } }, returnType: 'AgentInfo', v3Equivalent: 'agent/status' },
|
|
158
|
+
{ name: 'agent/create', parameters: { type: { type: 'string', required: true } }, returnType: 'AgentInfo', v3Equivalent: 'agent/spawn' },
|
|
159
|
+
|
|
160
|
+
// Swarm tools
|
|
161
|
+
{ name: 'swarm_status', parameters: {}, returnType: 'SwarmStatus', v3Equivalent: 'swarm/status' },
|
|
162
|
+
{ name: 'swarm/get-status', parameters: {}, returnType: 'SwarmStatus', v3Equivalent: 'swarm/status' },
|
|
163
|
+
{ name: 'swarm/get-comprehensive-status', parameters: {}, returnType: 'ComprehensiveStatus', v3Equivalent: 'swarm/status' },
|
|
164
|
+
{ name: 'mcp__ruv-swarm__swarm_init', parameters: { topology: { type: 'string', required: false } }, returnType: 'SwarmInfo', v3Equivalent: 'swarm/init' },
|
|
165
|
+
{ name: 'mcp__ruv-swarm__swarm_status', parameters: {}, returnType: 'SwarmStatus', v3Equivalent: 'swarm/status' },
|
|
166
|
+
{ name: 'mcp__ruv-swarm__agent_spawn', parameters: { type: { type: 'string', required: true } }, returnType: 'AgentInfo', v3Equivalent: 'agent/spawn' },
|
|
167
|
+
{ name: 'mcp__ruv-swarm__agent_list', parameters: {}, returnType: 'AgentInfo[]', v3Equivalent: 'agent/list' },
|
|
168
|
+
{ name: 'mcp__ruv-swarm__agent_metrics', parameters: { id: { type: 'string', required: true } }, returnType: 'AgentMetrics', v3Equivalent: 'agent/status' },
|
|
169
|
+
|
|
170
|
+
// Memory tools
|
|
171
|
+
{ name: 'memory/query', parameters: { search: { type: 'string', required: true } }, returnType: 'MemoryEntry[]', v3Equivalent: 'memory/search' },
|
|
172
|
+
{ name: 'memory/store', parameters: { content: { type: 'string', required: true }, type: { type: 'string', required: false } }, returnType: 'MemoryEntry', v3Equivalent: 'memory/store' },
|
|
173
|
+
{ name: 'memory/delete', parameters: { id: { type: 'string', required: true } }, returnType: 'boolean', v3Equivalent: 'memory/delete' },
|
|
174
|
+
{ name: 'mcp__ruv-swarm__memory_usage', parameters: {}, returnType: 'MemoryStats', v3Equivalent: 'memory/list' },
|
|
175
|
+
|
|
176
|
+
// Config tools
|
|
177
|
+
{ name: 'config/get', parameters: { key: { type: 'string', required: true } }, returnType: 'any', v3Equivalent: 'config/load' },
|
|
178
|
+
{ name: 'config/update', parameters: { key: { type: 'string', required: true }, value: { type: 'any', required: true } }, returnType: 'boolean', v3Equivalent: 'config/save' },
|
|
179
|
+
|
|
180
|
+
// Task tools
|
|
181
|
+
{ name: 'task/create', parameters: { description: { type: 'string', required: true } }, returnType: 'TaskInfo', v3Equivalent: 'task/create' },
|
|
182
|
+
{ name: 'task/assign', parameters: { taskId: { type: 'string', required: true }, agentId: { type: 'string', required: true } }, returnType: 'boolean', v3Equivalent: 'task/assign' },
|
|
183
|
+
{ name: 'task/status', parameters: { taskId: { type: 'string', required: true } }, returnType: 'TaskStatus', v3Equivalent: 'task/status' },
|
|
184
|
+
{ name: 'task/complete', parameters: { taskId: { type: 'string', required: true }, result: { type: 'any', required: false } }, returnType: 'boolean', v3Equivalent: 'task/complete' },
|
|
185
|
+
|
|
186
|
+
// Neural/Learning tools
|
|
187
|
+
{ name: 'mcp__ruv-swarm__neural_status', parameters: {}, returnType: 'NeuralStatus', v3Equivalent: 'hooks/metrics' },
|
|
188
|
+
{ name: 'mcp__ruv-swarm__neural_train', parameters: { data: { type: 'object', required: true } }, returnType: 'TrainingResult', v3Equivalent: 'hooks/pretrain' },
|
|
189
|
+
|
|
190
|
+
// GitHub integration tools
|
|
191
|
+
{ name: 'github/pr-create', parameters: { title: { type: 'string', required: true }, body: { type: 'string', required: false } }, returnType: 'PRInfo', v3Equivalent: 'github/pr-create' },
|
|
192
|
+
{ name: 'github/pr-review', parameters: { prNumber: { type: 'number', required: true } }, returnType: 'ReviewInfo', v3Equivalent: 'github/pr-review' },
|
|
193
|
+
{ name: 'github/issue-create', parameters: { title: { type: 'string', required: true } }, returnType: 'IssueInfo', v3Equivalent: 'github/issue-create' },
|
|
194
|
+
|
|
195
|
+
// Coordination tools
|
|
196
|
+
{ name: 'coordinate/consensus', parameters: { proposal: { type: 'object', required: true } }, returnType: 'ConsensusResult', v3Equivalent: 'swarm/consensus' },
|
|
197
|
+
{ name: 'coordinate/broadcast', parameters: { message: { type: 'object', required: true } }, returnType: 'BroadcastResult', v3Equivalent: 'swarm/broadcast' },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* V2 Hooks (42 total)
|
|
202
|
+
*/
|
|
203
|
+
export const V2_HOOKS: V2Hook[] = [
|
|
204
|
+
// Edit hooks
|
|
205
|
+
{ name: 'pre-edit', trigger: 'before:file:edit', parameters: ['filePath', 'content'], returnType: 'HookResult', v3Equivalent: 'pre-edit' },
|
|
206
|
+
{ name: 'post-edit', trigger: 'after:file:edit', parameters: ['filePath', 'success', 'changes'], returnType: 'HookResult', v3Equivalent: 'post-edit' },
|
|
207
|
+
{ name: 'pre-create', trigger: 'before:file:create', parameters: ['filePath'], returnType: 'HookResult', v3Equivalent: 'pre-edit' },
|
|
208
|
+
{ name: 'post-create', trigger: 'after:file:create', parameters: ['filePath', 'success'], returnType: 'HookResult', v3Equivalent: 'post-edit' },
|
|
209
|
+
|
|
210
|
+
// Command hooks
|
|
211
|
+
{ name: 'pre-command', trigger: 'before:command:execute', parameters: ['command', 'args'], returnType: 'HookResult', v3Equivalent: 'pre-command' },
|
|
212
|
+
{ name: 'post-command', trigger: 'after:command:execute', parameters: ['command', 'success', 'output'], returnType: 'HookResult', v3Equivalent: 'post-command' },
|
|
213
|
+
{ name: 'pre-bash', trigger: 'before:bash:execute', parameters: ['script'], returnType: 'HookResult', v3Equivalent: 'pre-command' },
|
|
214
|
+
{ name: 'post-bash', trigger: 'after:bash:execute', parameters: ['script', 'exitCode'], returnType: 'HookResult', v3Equivalent: 'post-command' },
|
|
215
|
+
|
|
216
|
+
// Task hooks
|
|
217
|
+
{ name: 'pre-task', trigger: 'before:task:start', parameters: ['task'], returnType: 'HookResult', v3Equivalent: 'pre-task' },
|
|
218
|
+
{ name: 'post-task', trigger: 'after:task:complete', parameters: ['task', 'result'], returnType: 'HookResult', v3Equivalent: 'post-task' },
|
|
219
|
+
{ name: 'task-assign', trigger: 'on:task:assign', parameters: ['task', 'agent'], returnType: 'HookResult', v3Equivalent: 'task-assign' },
|
|
220
|
+
{ name: 'task-fail', trigger: 'on:task:fail', parameters: ['task', 'error'], returnType: 'HookResult', v3Equivalent: 'task-fail' },
|
|
221
|
+
|
|
222
|
+
// Agent hooks
|
|
223
|
+
{ name: 'agent-spawn', trigger: 'on:agent:spawn', parameters: ['agentConfig'], returnType: 'HookResult', v3Equivalent: 'agent-spawn' },
|
|
224
|
+
{ name: 'agent-terminate', trigger: 'on:agent:terminate', parameters: ['agentId', 'reason'], returnType: 'HookResult', v3Equivalent: 'agent-terminate' },
|
|
225
|
+
{ name: 'agent-message', trigger: 'on:agent:message', parameters: ['from', 'to', 'message'], returnType: 'HookResult', v3Equivalent: 'agent-message' },
|
|
226
|
+
{ name: 'agent-error', trigger: 'on:agent:error', parameters: ['agentId', 'error'], returnType: 'HookResult', v3Equivalent: 'agent-error' },
|
|
227
|
+
|
|
228
|
+
// Swarm hooks
|
|
229
|
+
{ name: 'swarm-init', trigger: 'on:swarm:init', parameters: ['topology', 'config'], returnType: 'HookResult', v3Equivalent: 'swarm-init' },
|
|
230
|
+
{ name: 'swarm-scale', trigger: 'on:swarm:scale', parameters: ['direction', 'count'], returnType: 'HookResult', v3Equivalent: 'swarm-scale' },
|
|
231
|
+
{ name: 'swarm-consensus', trigger: 'on:swarm:consensus', parameters: ['proposal', 'result'], returnType: 'HookResult', v3Equivalent: 'swarm-consensus' },
|
|
232
|
+
{ name: 'swarm-broadcast', trigger: 'on:swarm:broadcast', parameters: ['message'], returnType: 'HookResult', v3Equivalent: 'swarm-broadcast' },
|
|
233
|
+
|
|
234
|
+
// Memory hooks
|
|
235
|
+
{ name: 'memory-store', trigger: 'on:memory:store', parameters: ['entry'], returnType: 'HookResult', v3Equivalent: 'memory-store' },
|
|
236
|
+
{ name: 'memory-retrieve', trigger: 'on:memory:retrieve', parameters: ['query', 'results'], returnType: 'HookResult', v3Equivalent: 'memory-retrieve' },
|
|
237
|
+
{ name: 'memory-delete', trigger: 'on:memory:delete', parameters: ['id'], returnType: 'HookResult', v3Equivalent: 'memory-delete' },
|
|
238
|
+
{ name: 'memory-consolidate', trigger: 'on:memory:consolidate', parameters: [], returnType: 'HookResult', v3Equivalent: 'memory-consolidate' },
|
|
239
|
+
|
|
240
|
+
// Learning hooks
|
|
241
|
+
{ name: 'learning-pattern', trigger: 'on:learning:pattern', parameters: ['pattern'], returnType: 'HookResult', v3Equivalent: 'learning-pattern' },
|
|
242
|
+
{ name: 'learning-reward', trigger: 'on:learning:reward', parameters: ['trajectory', 'reward'], returnType: 'HookResult', v3Equivalent: 'learning-reward' },
|
|
243
|
+
{ name: 'learning-distill', trigger: 'on:learning:distill', parameters: ['memories'], returnType: 'HookResult', v3Equivalent: 'learning-distill' },
|
|
244
|
+
{ name: 'learning-consolidate', trigger: 'on:learning:consolidate', parameters: [], returnType: 'HookResult', v3Equivalent: 'learning-consolidate' },
|
|
245
|
+
|
|
246
|
+
// Session hooks
|
|
247
|
+
{ name: 'session-start', trigger: 'on:session:start', parameters: ['sessionId'], returnType: 'HookResult', v3Equivalent: 'session-start' },
|
|
248
|
+
{ name: 'session-end', trigger: 'on:session:end', parameters: ['sessionId', 'metrics'], returnType: 'HookResult', v3Equivalent: 'session-end' },
|
|
249
|
+
{ name: 'session-resume', trigger: 'on:session:resume', parameters: ['sessionId'], returnType: 'HookResult', v3Equivalent: 'session-resume' },
|
|
250
|
+
{ name: 'session-pause', trigger: 'on:session:pause', parameters: ['sessionId'], returnType: 'HookResult', v3Equivalent: 'session-pause' },
|
|
251
|
+
|
|
252
|
+
// Config hooks
|
|
253
|
+
{ name: 'config-load', trigger: 'on:config:load', parameters: ['config'], returnType: 'HookResult', v3Equivalent: 'config-load' },
|
|
254
|
+
{ name: 'config-save', trigger: 'on:config:save', parameters: ['config'], returnType: 'HookResult', v3Equivalent: 'config-save' },
|
|
255
|
+
{ name: 'config-change', trigger: 'on:config:change', parameters: ['key', 'oldValue', 'newValue'], returnType: 'HookResult', v3Equivalent: 'config-change' },
|
|
256
|
+
|
|
257
|
+
// Error hooks
|
|
258
|
+
{ name: 'error-global', trigger: 'on:error:global', parameters: ['error'], returnType: 'HookResult', v3Equivalent: 'error-global' },
|
|
259
|
+
{ name: 'error-recover', trigger: 'on:error:recover', parameters: ['error', 'strategy'], returnType: 'HookResult', v3Equivalent: 'error-recover' },
|
|
260
|
+
|
|
261
|
+
// Performance hooks
|
|
262
|
+
{ name: 'perf-threshold', trigger: 'on:perf:threshold', parameters: ['metric', 'value'], returnType: 'HookResult', v3Equivalent: 'perf-threshold' },
|
|
263
|
+
{ name: 'perf-report', trigger: 'on:perf:report', parameters: ['report'], returnType: 'HookResult', v3Equivalent: 'perf-report' },
|
|
264
|
+
|
|
265
|
+
// Security hooks
|
|
266
|
+
{ name: 'security-alert', trigger: 'on:security:alert', parameters: ['alert'], returnType: 'HookResult', v3Equivalent: 'security-alert' },
|
|
267
|
+
{ name: 'security-block', trigger: 'on:security:block', parameters: ['operation', 'reason'], returnType: 'HookResult', v3Equivalent: 'security-block' },
|
|
268
|
+
{ name: 'security-audit', trigger: 'on:security:audit', parameters: ['action', 'context'], returnType: 'HookResult', v3Equivalent: 'security-audit' },
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* V2 API Interfaces
|
|
273
|
+
*/
|
|
274
|
+
export const V2_API_INTERFACES: V2APIInterface[] = [
|
|
275
|
+
// Core interfaces
|
|
276
|
+
{
|
|
277
|
+
name: 'HiveMind',
|
|
278
|
+
methods: [
|
|
279
|
+
{ name: 'initialize', signature: '(config?: HiveMindConfig): Promise<void>' },
|
|
280
|
+
{ name: 'spawn', signature: '(type: string, config?: AgentConfig): Promise<Agent>' },
|
|
281
|
+
{ name: 'getStatus', signature: '(): Promise<HiveMindStatus>' },
|
|
282
|
+
{ name: 'shutdown', signature: '(): Promise<void>' },
|
|
283
|
+
],
|
|
284
|
+
v3Equivalent: 'UnifiedSwarmCoordinator',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'SwarmCoordinator',
|
|
288
|
+
methods: [
|
|
289
|
+
{ name: 'init', signature: '(topology: string): Promise<void>' },
|
|
290
|
+
{ name: 'addAgent', signature: '(agent: Agent): Promise<void>' },
|
|
291
|
+
{ name: 'removeAgent', signature: '(agentId: string): Promise<void>' },
|
|
292
|
+
{ name: 'broadcast', signature: '(message: Message): Promise<void>' },
|
|
293
|
+
{ name: 'consensus', signature: '(proposal: Proposal): Promise<ConsensusResult>' },
|
|
294
|
+
],
|
|
295
|
+
v3Equivalent: 'UnifiedSwarmCoordinator',
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'MemoryManager',
|
|
299
|
+
methods: [
|
|
300
|
+
{ name: 'store', signature: '(entry: MemoryEntry): Promise<string>' },
|
|
301
|
+
{ name: 'query', signature: '(search: string): Promise<MemoryEntry[]>' },
|
|
302
|
+
{ name: 'delete', signature: '(id: string): Promise<boolean>' },
|
|
303
|
+
{ name: 'clear', signature: '(): Promise<void>' },
|
|
304
|
+
{ name: 'getStats', signature: '(): Promise<MemoryStats>' },
|
|
305
|
+
],
|
|
306
|
+
v3Equivalent: 'UnifiedMemoryService',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'AgentManager',
|
|
310
|
+
methods: [
|
|
311
|
+
{ name: 'spawn', signature: '(config: AgentConfig): Promise<Agent>' },
|
|
312
|
+
{ name: 'terminate', signature: '(id: string): Promise<void>' },
|
|
313
|
+
{ name: 'list', signature: '(): Promise<Agent[]>' },
|
|
314
|
+
{ name: 'getInfo', signature: '(id: string): Promise<AgentInfo>' },
|
|
315
|
+
],
|
|
316
|
+
v3Equivalent: 'AgentLifecycleService',
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'TaskOrchestrator',
|
|
320
|
+
methods: [
|
|
321
|
+
{ name: 'create', signature: '(definition: TaskDefinition): Promise<Task>' },
|
|
322
|
+
{ name: 'assign', signature: '(taskId: string, agentId: string): Promise<void>' },
|
|
323
|
+
{ name: 'complete', signature: '(taskId: string, result?: any): Promise<void>' },
|
|
324
|
+
{ name: 'getStatus', signature: '(taskId: string): Promise<TaskStatus>' },
|
|
325
|
+
],
|
|
326
|
+
v3Equivalent: 'TaskExecutionService',
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Mock V3 service for testing
|
|
332
|
+
*/
|
|
333
|
+
interface MockV3Service {
|
|
334
|
+
cli: {
|
|
335
|
+
execute: (command: string, args: string[]) => Promise<{ success: boolean; output: string }>;
|
|
336
|
+
getCommands: () => string[];
|
|
337
|
+
};
|
|
338
|
+
mcp: {
|
|
339
|
+
callTool: (name: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
340
|
+
getTools: () => string[];
|
|
341
|
+
translateToolName: (v2Name: string) => string;
|
|
342
|
+
};
|
|
343
|
+
hooks: {
|
|
344
|
+
trigger: (name: string, params: Record<string, unknown>) => Promise<{ handled: boolean; result: unknown }>;
|
|
345
|
+
getHooks: () => string[];
|
|
346
|
+
};
|
|
347
|
+
api: {
|
|
348
|
+
getClass: (name: string) => { methods: string[] } | null;
|
|
349
|
+
getClasses: () => string[];
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* V2 Compatibility Validator
|
|
355
|
+
*
|
|
356
|
+
* Tests V3 implementation against V2 capabilities to ensure backward compatibility.
|
|
357
|
+
*/
|
|
358
|
+
export class V2CompatibilityValidator {
|
|
359
|
+
private readonly v3Service: MockV3Service;
|
|
360
|
+
private readonly v2Version: string;
|
|
361
|
+
private readonly v3Version: string;
|
|
362
|
+
private readonly verbose: boolean;
|
|
363
|
+
|
|
364
|
+
constructor(options: {
|
|
365
|
+
v3Service?: MockV3Service;
|
|
366
|
+
v2Version?: string;
|
|
367
|
+
v3Version?: string;
|
|
368
|
+
verbose?: boolean;
|
|
369
|
+
} = {}) {
|
|
370
|
+
this.v3Service = options.v3Service || this.createDefaultMockService();
|
|
371
|
+
this.v2Version = options.v2Version || '2.0.0';
|
|
372
|
+
this.v3Version = options.v3Version || '3.0.0';
|
|
373
|
+
this.verbose = options.verbose || false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Create default mock V3 service for testing
|
|
378
|
+
*/
|
|
379
|
+
private createDefaultMockService(): MockV3Service {
|
|
380
|
+
// Tool name mapping from V2 to V3
|
|
381
|
+
const toolNameMapping: Record<string, string> = {
|
|
382
|
+
'dispatch_agent': 'agent/spawn',
|
|
383
|
+
'agents/spawn': 'agent/spawn',
|
|
384
|
+
'agents/list': 'agent/list',
|
|
385
|
+
'agents/terminate': 'agent/terminate',
|
|
386
|
+
'agents/info': 'agent/status',
|
|
387
|
+
'agent/create': 'agent/spawn',
|
|
388
|
+
'swarm_status': 'swarm/status',
|
|
389
|
+
'swarm/get-status': 'swarm/status',
|
|
390
|
+
'swarm/get-comprehensive-status': 'swarm/status',
|
|
391
|
+
'mcp__ruv-swarm__swarm_init': 'swarm/init',
|
|
392
|
+
'mcp__ruv-swarm__swarm_status': 'swarm/status',
|
|
393
|
+
'mcp__ruv-swarm__agent_spawn': 'agent/spawn',
|
|
394
|
+
'mcp__ruv-swarm__agent_list': 'agent/list',
|
|
395
|
+
'mcp__ruv-swarm__agent_metrics': 'agent/status',
|
|
396
|
+
'memory/query': 'memory/search',
|
|
397
|
+
'mcp__ruv-swarm__memory_usage': 'memory/list',
|
|
398
|
+
'config/get': 'config/load',
|
|
399
|
+
'config/update': 'config/save',
|
|
400
|
+
'mcp__ruv-swarm__neural_status': 'hooks/metrics',
|
|
401
|
+
'mcp__ruv-swarm__neural_train': 'hooks/pretrain',
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const v3Tools = [
|
|
405
|
+
'agent/spawn', 'agent/list', 'agent/terminate', 'agent/status',
|
|
406
|
+
'swarm/init', 'swarm/status', 'swarm/scale', 'swarm/consensus', 'swarm/broadcast',
|
|
407
|
+
'memory/store', 'memory/search', 'memory/delete', 'memory/list',
|
|
408
|
+
'task/create', 'task/assign', 'task/status', 'task/complete',
|
|
409
|
+
'config/load', 'config/save',
|
|
410
|
+
'hooks/metrics', 'hooks/pretrain',
|
|
411
|
+
'github/pr-create', 'github/pr-review', 'github/issue-create',
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
const v3Commands = [
|
|
415
|
+
'init', 'start', 'stop', 'status', 'config',
|
|
416
|
+
'agent spawn', 'agent list', 'agent terminate', 'agent status',
|
|
417
|
+
'swarm init', 'swarm status', 'swarm scale',
|
|
418
|
+
'memory list', 'memory search', 'memory clear',
|
|
419
|
+
'hooks pre-edit', 'hooks post-edit', 'hooks pre-command', 'hooks post-command',
|
|
420
|
+
'hooks route', 'hooks pretrain', 'hooks metrics',
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
const v3Hooks = V2_HOOKS.map(h => h.v3Equivalent || h.name);
|
|
424
|
+
|
|
425
|
+
const v3Classes = ['UnifiedSwarmCoordinator', 'UnifiedMemoryService', 'AgentLifecycleService', 'TaskExecutionService'];
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
cli: {
|
|
429
|
+
execute: vi.fn().mockImplementation(async (command: string) => {
|
|
430
|
+
const isSupported = v3Commands.some(c => c === command || command.startsWith(c.split(' ')[0]));
|
|
431
|
+
return { success: isSupported, output: isSupported ? 'OK' : 'Command not found' };
|
|
432
|
+
}),
|
|
433
|
+
getCommands: vi.fn().mockReturnValue(v3Commands),
|
|
434
|
+
},
|
|
435
|
+
mcp: {
|
|
436
|
+
callTool: vi.fn().mockImplementation(async (name: string) => {
|
|
437
|
+
const v3Name = toolNameMapping[name] || name;
|
|
438
|
+
const isSupported = v3Tools.includes(v3Name);
|
|
439
|
+
if (!isSupported) throw new Error(`Tool not found: ${name}`);
|
|
440
|
+
return { success: true };
|
|
441
|
+
}),
|
|
442
|
+
getTools: vi.fn().mockReturnValue(v3Tools),
|
|
443
|
+
translateToolName: vi.fn().mockImplementation((v2Name: string) => toolNameMapping[v2Name] || v2Name),
|
|
444
|
+
},
|
|
445
|
+
hooks: {
|
|
446
|
+
trigger: vi.fn().mockImplementation(async (name: string) => {
|
|
447
|
+
const isSupported = v3Hooks.includes(name);
|
|
448
|
+
return { handled: isSupported, result: isSupported ? {} : null };
|
|
449
|
+
}),
|
|
450
|
+
getHooks: vi.fn().mockReturnValue(v3Hooks),
|
|
451
|
+
},
|
|
452
|
+
api: {
|
|
453
|
+
getClass: vi.fn().mockImplementation((name: string) => {
|
|
454
|
+
const mapping: Record<string, { methods: string[] }> = {
|
|
455
|
+
'UnifiedSwarmCoordinator': { methods: ['initialize', 'spawn', 'addAgent', 'removeAgent', 'broadcast', 'consensus', 'getStatus', 'shutdown'] },
|
|
456
|
+
'UnifiedMemoryService': { methods: ['store', 'search', 'delete', 'clear', 'getStats'] },
|
|
457
|
+
'AgentLifecycleService': { methods: ['spawn', 'terminate', 'list', 'getInfo', 'getStatus'] },
|
|
458
|
+
'TaskExecutionService': { methods: ['create', 'assign', 'complete', 'getStatus'] },
|
|
459
|
+
};
|
|
460
|
+
return mapping[name] || null;
|
|
461
|
+
}),
|
|
462
|
+
getClasses: vi.fn().mockReturnValue(v3Classes),
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Validate CLI command compatibility
|
|
469
|
+
*/
|
|
470
|
+
async validateCLI(): Promise<ValidationResult> {
|
|
471
|
+
const startTime = Date.now();
|
|
472
|
+
const checks: ValidationCheck[] = [];
|
|
473
|
+
const v3Commands = this.v3Service.cli.getCommands();
|
|
474
|
+
|
|
475
|
+
for (const cmd of V2_CLI_COMMANDS) {
|
|
476
|
+
const v3Equivalent = cmd.v3Equivalent || cmd.name;
|
|
477
|
+
const isSupported = v3Commands.some(c =>
|
|
478
|
+
c === v3Equivalent || c.startsWith(v3Equivalent.split(' ')[0])
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// Check command exists
|
|
482
|
+
checks.push({
|
|
483
|
+
name: `CLI: ${cmd.name}`,
|
|
484
|
+
category: 'cli',
|
|
485
|
+
passed: isSupported,
|
|
486
|
+
message: isSupported
|
|
487
|
+
? `Command "${cmd.name}" is supported via "${v3Equivalent}"`
|
|
488
|
+
: `Command "${cmd.name}" is not available in V3`,
|
|
489
|
+
v2Behavior: `Execute "${cmd.name}" with flags: ${cmd.flags.join(', ') || 'none'}`,
|
|
490
|
+
v3Behavior: isSupported
|
|
491
|
+
? `Execute "${v3Equivalent}"`
|
|
492
|
+
: 'Not available',
|
|
493
|
+
breaking: !isSupported && !cmd.deprecated,
|
|
494
|
+
migrationPath: isSupported ? `Use "${v3Equivalent}" instead` : undefined,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Check aliases
|
|
498
|
+
for (const alias of cmd.aliases) {
|
|
499
|
+
const aliasSupported = v3Commands.some(c => c === alias || c.startsWith(alias.split(' ')[0]));
|
|
500
|
+
checks.push({
|
|
501
|
+
name: `CLI Alias: ${alias}`,
|
|
502
|
+
category: 'cli',
|
|
503
|
+
passed: aliasSupported || isSupported,
|
|
504
|
+
message: aliasSupported
|
|
505
|
+
? `Alias "${alias}" is supported`
|
|
506
|
+
: `Alias "${alias}" not directly supported, use "${v3Equivalent}"`,
|
|
507
|
+
v2Behavior: `Execute "${alias}"`,
|
|
508
|
+
v3Behavior: aliasSupported ? `Execute "${alias}"` : `Execute "${v3Equivalent}"`,
|
|
509
|
+
breaking: false,
|
|
510
|
+
migrationPath: `Use "${v3Equivalent}" for consistent behavior`,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check flags
|
|
515
|
+
for (const flag of cmd.flags) {
|
|
516
|
+
checks.push({
|
|
517
|
+
name: `CLI Flag: ${cmd.name} ${flag}`,
|
|
518
|
+
category: 'cli',
|
|
519
|
+
passed: isSupported, // Assume flags are supported if command is
|
|
520
|
+
message: isSupported
|
|
521
|
+
? `Flag "${flag}" is expected to work with "${v3Equivalent}"`
|
|
522
|
+
: `Flag "${flag}" not available (command not supported)`,
|
|
523
|
+
v2Behavior: `Use "${flag}" with "${cmd.name}"`,
|
|
524
|
+
v3Behavior: isSupported ? `Use "${flag}" with "${v3Equivalent}"` : 'Not available',
|
|
525
|
+
breaking: !isSupported && !cmd.deprecated,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const passedChecks = checks.filter(c => c.passed).length;
|
|
531
|
+
const breakingChanges = checks.filter(c => c.breaking).length;
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
category: 'cli',
|
|
535
|
+
totalChecks: checks.length,
|
|
536
|
+
passedChecks,
|
|
537
|
+
failedChecks: checks.length - passedChecks,
|
|
538
|
+
breakingChanges,
|
|
539
|
+
checks,
|
|
540
|
+
duration: Date.now() - startTime,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Validate MCP tool compatibility
|
|
546
|
+
*/
|
|
547
|
+
async validateMCPTools(): Promise<ValidationResult> {
|
|
548
|
+
const startTime = Date.now();
|
|
549
|
+
const checks: ValidationCheck[] = [];
|
|
550
|
+
const v3Tools = this.v3Service.mcp.getTools();
|
|
551
|
+
|
|
552
|
+
for (const tool of V2_MCP_TOOLS) {
|
|
553
|
+
const v3Equivalent = this.v3Service.mcp.translateToolName(tool.name);
|
|
554
|
+
const isSupported = v3Tools.includes(v3Equivalent);
|
|
555
|
+
|
|
556
|
+
// Check tool exists
|
|
557
|
+
checks.push({
|
|
558
|
+
name: `MCP Tool: ${tool.name}`,
|
|
559
|
+
category: 'mcp',
|
|
560
|
+
passed: isSupported,
|
|
561
|
+
message: isSupported
|
|
562
|
+
? `Tool "${tool.name}" maps to "${v3Equivalent}"`
|
|
563
|
+
: `Tool "${tool.name}" has no V3 equivalent`,
|
|
564
|
+
v2Behavior: `Call "${tool.name}" with params`,
|
|
565
|
+
v3Behavior: isSupported
|
|
566
|
+
? `Call "${v3Equivalent}" with translated params`
|
|
567
|
+
: 'Not available',
|
|
568
|
+
breaking: !isSupported && !tool.deprecated,
|
|
569
|
+
migrationPath: isSupported ? `Use "${v3Equivalent}" with updated parameters` : undefined,
|
|
570
|
+
details: {
|
|
571
|
+
v2Parameters: tool.parameters,
|
|
572
|
+
v3Equivalent,
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Check parameter translation
|
|
577
|
+
if (isSupported) {
|
|
578
|
+
for (const [paramName, paramDef] of Object.entries(tool.parameters)) {
|
|
579
|
+
checks.push({
|
|
580
|
+
name: `MCP Param: ${tool.name}.${paramName}`,
|
|
581
|
+
category: 'mcp',
|
|
582
|
+
passed: true, // Assume param translation works
|
|
583
|
+
message: `Parameter "${paramName}" (${paramDef.type}) is translated`,
|
|
584
|
+
v2Behavior: `Pass "${paramName}" as ${paramDef.type}`,
|
|
585
|
+
v3Behavior: `Translated to V3 format`,
|
|
586
|
+
breaking: false,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const passedChecks = checks.filter(c => c.passed).length;
|
|
593
|
+
const breakingChanges = checks.filter(c => c.breaking).length;
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
category: 'mcp',
|
|
597
|
+
totalChecks: checks.length,
|
|
598
|
+
passedChecks,
|
|
599
|
+
failedChecks: checks.length - passedChecks,
|
|
600
|
+
breakingChanges,
|
|
601
|
+
checks,
|
|
602
|
+
duration: Date.now() - startTime,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Validate hook compatibility
|
|
608
|
+
*/
|
|
609
|
+
async validateHooks(): Promise<ValidationResult> {
|
|
610
|
+
const startTime = Date.now();
|
|
611
|
+
const checks: ValidationCheck[] = [];
|
|
612
|
+
const v3Hooks = this.v3Service.hooks.getHooks();
|
|
613
|
+
|
|
614
|
+
for (const hook of V2_HOOKS) {
|
|
615
|
+
const v3Equivalent = hook.v3Equivalent || hook.name;
|
|
616
|
+
const isSupported = v3Hooks.includes(v3Equivalent);
|
|
617
|
+
|
|
618
|
+
// Check hook exists
|
|
619
|
+
checks.push({
|
|
620
|
+
name: `Hook: ${hook.name}`,
|
|
621
|
+
category: 'hooks',
|
|
622
|
+
passed: isSupported,
|
|
623
|
+
message: isSupported
|
|
624
|
+
? `Hook "${hook.name}" is supported as "${v3Equivalent}"`
|
|
625
|
+
: `Hook "${hook.name}" is not available in V3`,
|
|
626
|
+
v2Behavior: `Trigger on "${hook.trigger}" with params: ${hook.parameters.join(', ')}`,
|
|
627
|
+
v3Behavior: isSupported
|
|
628
|
+
? `Trigger "${v3Equivalent}" with translated params`
|
|
629
|
+
: 'Not available',
|
|
630
|
+
breaking: !isSupported && !hook.deprecated,
|
|
631
|
+
migrationPath: isSupported ? `Listen for "${v3Equivalent}" instead` : undefined,
|
|
632
|
+
details: {
|
|
633
|
+
v2Trigger: hook.trigger,
|
|
634
|
+
v2Parameters: hook.parameters,
|
|
635
|
+
v3Equivalent,
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Check parameters
|
|
640
|
+
for (const param of hook.parameters) {
|
|
641
|
+
checks.push({
|
|
642
|
+
name: `Hook Param: ${hook.name}.${param}`,
|
|
643
|
+
category: 'hooks',
|
|
644
|
+
passed: isSupported, // Assume params work if hook works
|
|
645
|
+
message: isSupported
|
|
646
|
+
? `Parameter "${param}" is passed to hook`
|
|
647
|
+
: `Parameter "${param}" not available (hook not supported)`,
|
|
648
|
+
v2Behavior: `Receive "${param}" in hook handler`,
|
|
649
|
+
v3Behavior: isSupported ? 'Translated parameter available' : 'Not available',
|
|
650
|
+
breaking: !isSupported,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Check return type compatibility
|
|
655
|
+
checks.push({
|
|
656
|
+
name: `Hook Return: ${hook.name}`,
|
|
657
|
+
category: 'hooks',
|
|
658
|
+
passed: isSupported,
|
|
659
|
+
message: isSupported
|
|
660
|
+
? `Return type "${hook.returnType}" is compatible`
|
|
661
|
+
: `Return type not available (hook not supported)`,
|
|
662
|
+
v2Behavior: `Return ${hook.returnType}`,
|
|
663
|
+
v3Behavior: isSupported ? `Return compatible ${hook.returnType}` : 'Not available',
|
|
664
|
+
breaking: !isSupported,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const passedChecks = checks.filter(c => c.passed).length;
|
|
669
|
+
const breakingChanges = checks.filter(c => c.breaking).length;
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
category: 'hooks',
|
|
673
|
+
totalChecks: checks.length,
|
|
674
|
+
passedChecks,
|
|
675
|
+
failedChecks: checks.length - passedChecks,
|
|
676
|
+
breakingChanges,
|
|
677
|
+
checks,
|
|
678
|
+
duration: Date.now() - startTime,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Validate API compatibility
|
|
684
|
+
*/
|
|
685
|
+
async validateAPI(): Promise<ValidationResult> {
|
|
686
|
+
const startTime = Date.now();
|
|
687
|
+
const checks: ValidationCheck[] = [];
|
|
688
|
+
const v3Classes = this.v3Service.api.getClasses();
|
|
689
|
+
|
|
690
|
+
for (const iface of V2_API_INTERFACES) {
|
|
691
|
+
const v3Equivalent = iface.v3Equivalent || iface.name;
|
|
692
|
+
const v3Class = this.v3Service.api.getClass(v3Equivalent);
|
|
693
|
+
const isSupported = v3Class !== null;
|
|
694
|
+
|
|
695
|
+
// Check class exists
|
|
696
|
+
checks.push({
|
|
697
|
+
name: `API Class: ${iface.name}`,
|
|
698
|
+
category: 'api',
|
|
699
|
+
passed: isSupported,
|
|
700
|
+
message: isSupported
|
|
701
|
+
? `Class "${iface.name}" is available as "${v3Equivalent}"`
|
|
702
|
+
: `Class "${iface.name}" has no V3 equivalent`,
|
|
703
|
+
v2Behavior: `Import and use "${iface.name}"`,
|
|
704
|
+
v3Behavior: isSupported
|
|
705
|
+
? `Import "${v3Equivalent}" from @sparkleideas/*`
|
|
706
|
+
: 'Not available',
|
|
707
|
+
breaking: !isSupported && !iface.deprecated,
|
|
708
|
+
migrationPath: isSupported
|
|
709
|
+
? `Use "${v3Equivalent}" with import alias`
|
|
710
|
+
: undefined,
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Check methods
|
|
714
|
+
for (const method of iface.methods) {
|
|
715
|
+
const methodAvailable = v3Class?.methods.some(m =>
|
|
716
|
+
m === method.name || m.toLowerCase() === method.name.toLowerCase()
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
checks.push({
|
|
720
|
+
name: `API Method: ${iface.name}.${method.name}`,
|
|
721
|
+
category: 'api',
|
|
722
|
+
passed: methodAvailable || false,
|
|
723
|
+
message: methodAvailable
|
|
724
|
+
? `Method "${method.name}" is available`
|
|
725
|
+
: `Method "${method.name}" may have different name or signature`,
|
|
726
|
+
v2Behavior: `Call ${iface.name}.${method.name}${method.signature}`,
|
|
727
|
+
v3Behavior: methodAvailable
|
|
728
|
+
? `Call ${v3Equivalent}.${method.name}()`
|
|
729
|
+
: 'May need migration',
|
|
730
|
+
breaking: !methodAvailable && !iface.deprecated,
|
|
731
|
+
migrationPath: methodAvailable
|
|
732
|
+
? 'Same method signature'
|
|
733
|
+
: 'Check V3 API documentation',
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const passedChecks = checks.filter(c => c.passed).length;
|
|
739
|
+
const breakingChanges = checks.filter(c => c.breaking).length;
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
category: 'api',
|
|
743
|
+
totalChecks: checks.length,
|
|
744
|
+
passedChecks,
|
|
745
|
+
failedChecks: checks.length - passedChecks,
|
|
746
|
+
breakingChanges,
|
|
747
|
+
checks,
|
|
748
|
+
duration: Date.now() - startTime,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Run full validation suite
|
|
754
|
+
*/
|
|
755
|
+
async runFullValidation(): Promise<FullValidationReport> {
|
|
756
|
+
const startTime = Date.now();
|
|
757
|
+
|
|
758
|
+
this.log('Starting V2 Compatibility Validation...');
|
|
759
|
+
|
|
760
|
+
this.log('Validating CLI commands...');
|
|
761
|
+
const cliResult = await this.validateCLI();
|
|
762
|
+
|
|
763
|
+
this.log('Validating MCP tools...');
|
|
764
|
+
const mcpResult = await this.validateMCPTools();
|
|
765
|
+
|
|
766
|
+
this.log('Validating hooks...');
|
|
767
|
+
const hooksResult = await this.validateHooks();
|
|
768
|
+
|
|
769
|
+
this.log('Validating API interfaces...');
|
|
770
|
+
const apiResult = await this.validateAPI();
|
|
771
|
+
|
|
772
|
+
const totalChecks = cliResult.totalChecks + mcpResult.totalChecks + hooksResult.totalChecks + apiResult.totalChecks;
|
|
773
|
+
const passedChecks = cliResult.passedChecks + mcpResult.passedChecks + hooksResult.passedChecks + apiResult.passedChecks;
|
|
774
|
+
const failedChecks = totalChecks - passedChecks;
|
|
775
|
+
const breakingChanges = cliResult.breakingChanges + mcpResult.breakingChanges + hooksResult.breakingChanges + apiResult.breakingChanges;
|
|
776
|
+
|
|
777
|
+
const overallPassed = breakingChanges === 0;
|
|
778
|
+
|
|
779
|
+
const recommendations = this.generateRecommendations(cliResult, mcpResult, hooksResult, apiResult);
|
|
780
|
+
|
|
781
|
+
const report: FullValidationReport = {
|
|
782
|
+
timestamp: new Date(),
|
|
783
|
+
v2Version: this.v2Version,
|
|
784
|
+
v3Version: this.v3Version,
|
|
785
|
+
overallPassed,
|
|
786
|
+
totalChecks,
|
|
787
|
+
passedChecks,
|
|
788
|
+
failedChecks,
|
|
789
|
+
breakingChanges,
|
|
790
|
+
cli: cliResult,
|
|
791
|
+
mcp: mcpResult,
|
|
792
|
+
hooks: hooksResult,
|
|
793
|
+
api: apiResult,
|
|
794
|
+
summary: this.generateSummary(cliResult, mcpResult, hooksResult, apiResult, overallPassed),
|
|
795
|
+
recommendations,
|
|
796
|
+
duration: Date.now() - startTime,
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
this.log('Validation complete.');
|
|
800
|
+
|
|
801
|
+
return report;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Generate recommendations based on results
|
|
806
|
+
*/
|
|
807
|
+
private generateRecommendations(
|
|
808
|
+
cli: ValidationResult,
|
|
809
|
+
mcp: ValidationResult,
|
|
810
|
+
hooks: ValidationResult,
|
|
811
|
+
api: ValidationResult
|
|
812
|
+
): string[] {
|
|
813
|
+
const recommendations: string[] = [];
|
|
814
|
+
|
|
815
|
+
if (cli.breakingChanges > 0) {
|
|
816
|
+
recommendations.push('Update CLI command calls to use V3 equivalents');
|
|
817
|
+
recommendations.push('Run migration script: npx @sparkleideas/cli migrate');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (mcp.breakingChanges > 0) {
|
|
821
|
+
recommendations.push('Enable V2 compatibility mode in MCP server configuration');
|
|
822
|
+
recommendations.push('Update tool calls to use new naming convention (e.g., agent/spawn)');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (hooks.breakingChanges > 0) {
|
|
826
|
+
recommendations.push('Review hook configuration for renamed or removed hooks');
|
|
827
|
+
recommendations.push('Update hook listeners to use V3 event names');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (api.breakingChanges > 0) {
|
|
831
|
+
recommendations.push('Update import statements to use @sparkleideas/* packages');
|
|
832
|
+
recommendations.push('Use provided import aliases for backward compatibility');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (cli.passedChecks < cli.totalChecks) {
|
|
836
|
+
recommendations.push('Some CLI aliases may not be directly supported - use canonical command names');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (mcp.passedChecks < mcp.totalChecks) {
|
|
840
|
+
recommendations.push('Consider using tool name translation layer for gradual migration');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (recommendations.length === 0) {
|
|
844
|
+
recommendations.push('No migration actions required - V2 code is fully compatible');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return recommendations;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Generate human-readable summary
|
|
852
|
+
*/
|
|
853
|
+
private generateSummary(
|
|
854
|
+
cli: ValidationResult,
|
|
855
|
+
mcp: ValidationResult,
|
|
856
|
+
hooks: ValidationResult,
|
|
857
|
+
api: ValidationResult,
|
|
858
|
+
overallPassed: boolean
|
|
859
|
+
): string {
|
|
860
|
+
const lines: string[] = [
|
|
861
|
+
'='.repeat(70),
|
|
862
|
+
' V2 COMPATIBILITY VALIDATION REPORT',
|
|
863
|
+
'='.repeat(70),
|
|
864
|
+
'',
|
|
865
|
+
`Status: ${overallPassed ? 'PASSED - No breaking changes detected' : 'FAILED - Breaking changes detected'}`,
|
|
866
|
+
'',
|
|
867
|
+
'Category Summary:',
|
|
868
|
+
'-'.repeat(70),
|
|
869
|
+
`CLI Commands: ${cli.passedChecks}/${cli.totalChecks} passed (${cli.breakingChanges} breaking)`,
|
|
870
|
+
`MCP Tools: ${mcp.passedChecks}/${mcp.totalChecks} passed (${mcp.breakingChanges} breaking)`,
|
|
871
|
+
`Hooks: ${hooks.passedChecks}/${hooks.totalChecks} passed (${hooks.breakingChanges} breaking)`,
|
|
872
|
+
`API Interfaces: ${api.passedChecks}/${api.totalChecks} passed (${api.breakingChanges} breaking)`,
|
|
873
|
+
'-'.repeat(70),
|
|
874
|
+
'',
|
|
875
|
+
];
|
|
876
|
+
|
|
877
|
+
if (!overallPassed) {
|
|
878
|
+
lines.push('Breaking Changes Detected:');
|
|
879
|
+
lines.push('');
|
|
880
|
+
|
|
881
|
+
const allBreaking = [
|
|
882
|
+
...cli.checks.filter(c => c.breaking).map(c => ` CLI: ${c.name}`),
|
|
883
|
+
...mcp.checks.filter(c => c.breaking).map(c => ` MCP: ${c.name}`),
|
|
884
|
+
...hooks.checks.filter(c => c.breaking).map(c => ` Hooks: ${c.name}`),
|
|
885
|
+
...api.checks.filter(c => c.breaking).map(c => ` API: ${c.name}`),
|
|
886
|
+
].slice(0, 20);
|
|
887
|
+
|
|
888
|
+
lines.push(...allBreaking);
|
|
889
|
+
|
|
890
|
+
if (cli.breakingChanges + mcp.breakingChanges + hooks.breakingChanges + api.breakingChanges > 20) {
|
|
891
|
+
lines.push(` ... and ${cli.breakingChanges + mcp.breakingChanges + hooks.breakingChanges + api.breakingChanges - 20} more`);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
lines.push('');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
lines.push('='.repeat(70));
|
|
898
|
+
|
|
899
|
+
return lines.join('\n');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Log message if verbose mode is enabled
|
|
904
|
+
*/
|
|
905
|
+
private log(message: string): void {
|
|
906
|
+
if (this.verbose) {
|
|
907
|
+
console.log(`[V2Compat] ${message}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Generate markdown compatibility report
|
|
914
|
+
*/
|
|
915
|
+
export function generateCompatibilityReport(report: FullValidationReport): string {
|
|
916
|
+
const lines: string[] = [
|
|
917
|
+
'# V2 Compatibility Validation Report',
|
|
918
|
+
'',
|
|
919
|
+
`> Generated: ${report.timestamp.toISOString()}`,
|
|
920
|
+
`> V2 Version: ${report.v2Version}`,
|
|
921
|
+
`> V3 Version: ${report.v3Version}`,
|
|
922
|
+
'',
|
|
923
|
+
'## Executive Summary',
|
|
924
|
+
'',
|
|
925
|
+
`**Status**: ${report.overallPassed ? 'PASSED' : 'FAILED'}`,
|
|
926
|
+
'',
|
|
927
|
+
'| Metric | Value |',
|
|
928
|
+
'|--------|-------|',
|
|
929
|
+
`| Total Checks | ${report.totalChecks} |`,
|
|
930
|
+
`| Passed | ${report.passedChecks} |`,
|
|
931
|
+
`| Failed | ${report.failedChecks} |`,
|
|
932
|
+
`| Breaking Changes | ${report.breakingChanges} |`,
|
|
933
|
+
`| Duration | ${report.duration}ms |`,
|
|
934
|
+
'',
|
|
935
|
+
'## Category Results',
|
|
936
|
+
'',
|
|
937
|
+
'### CLI Commands',
|
|
938
|
+
'',
|
|
939
|
+
`- **Total**: ${report.cli.totalChecks}`,
|
|
940
|
+
`- **Passed**: ${report.cli.passedChecks}`,
|
|
941
|
+
`- **Failed**: ${report.cli.failedChecks}`,
|
|
942
|
+
`- **Breaking**: ${report.cli.breakingChanges}`,
|
|
943
|
+
'',
|
|
944
|
+
'| Command | Status | Migration |',
|
|
945
|
+
'|---------|--------|-----------|',
|
|
946
|
+
...report.cli.checks
|
|
947
|
+
.filter(c => c.name.startsWith('CLI:'))
|
|
948
|
+
.slice(0, 30)
|
|
949
|
+
.map(c => `| ${c.name.replace('CLI: ', '')} | ${c.passed ? 'OK' : 'FAIL'} | ${c.migrationPath || 'N/A'} |`),
|
|
950
|
+
'',
|
|
951
|
+
'### MCP Tools',
|
|
952
|
+
'',
|
|
953
|
+
`- **Total**: ${report.mcp.totalChecks}`,
|
|
954
|
+
`- **Passed**: ${report.mcp.passedChecks}`,
|
|
955
|
+
`- **Failed**: ${report.mcp.failedChecks}`,
|
|
956
|
+
`- **Breaking**: ${report.mcp.breakingChanges}`,
|
|
957
|
+
'',
|
|
958
|
+
'| Tool | Status | V3 Equivalent |',
|
|
959
|
+
'|------|--------|---------------|',
|
|
960
|
+
...report.mcp.checks
|
|
961
|
+
.filter(c => c.name.startsWith('MCP Tool:'))
|
|
962
|
+
.slice(0, 40)
|
|
963
|
+
.map(c => {
|
|
964
|
+
const v3Name = c.details?.v3Equivalent as string || 'N/A';
|
|
965
|
+
return `| ${c.name.replace('MCP Tool: ', '')} | ${c.passed ? 'OK' : 'FAIL'} | ${v3Name} |`;
|
|
966
|
+
}),
|
|
967
|
+
'',
|
|
968
|
+
'### Hooks',
|
|
969
|
+
'',
|
|
970
|
+
`- **Total**: ${report.hooks.totalChecks}`,
|
|
971
|
+
`- **Passed**: ${report.hooks.passedChecks}`,
|
|
972
|
+
`- **Failed**: ${report.hooks.failedChecks}`,
|
|
973
|
+
`- **Breaking**: ${report.hooks.breakingChanges}`,
|
|
974
|
+
'',
|
|
975
|
+
'| Hook | Status | V3 Trigger |',
|
|
976
|
+
'|------|--------|------------|',
|
|
977
|
+
...report.hooks.checks
|
|
978
|
+
.filter(c => c.name.startsWith('Hook:') && !c.name.includes('Param') && !c.name.includes('Return'))
|
|
979
|
+
.slice(0, 50)
|
|
980
|
+
.map(c => {
|
|
981
|
+
const v3Name = c.details?.v3Equivalent as string || 'N/A';
|
|
982
|
+
return `| ${c.name.replace('Hook: ', '')} | ${c.passed ? 'OK' : 'FAIL'} | ${v3Name} |`;
|
|
983
|
+
}),
|
|
984
|
+
'',
|
|
985
|
+
'### API Interfaces',
|
|
986
|
+
'',
|
|
987
|
+
`- **Total**: ${report.api.totalChecks}`,
|
|
988
|
+
`- **Passed**: ${report.api.passedChecks}`,
|
|
989
|
+
`- **Failed**: ${report.api.failedChecks}`,
|
|
990
|
+
`- **Breaking**: ${report.api.breakingChanges}`,
|
|
991
|
+
'',
|
|
992
|
+
'| Interface/Method | Status | Migration |',
|
|
993
|
+
'|------------------|--------|-----------|',
|
|
994
|
+
...report.api.checks
|
|
995
|
+
.slice(0, 30)
|
|
996
|
+
.map(c => `| ${c.name.replace('API ', '')} | ${c.passed ? 'OK' : 'FAIL'} | ${c.migrationPath || 'N/A'} |`),
|
|
997
|
+
'',
|
|
998
|
+
'## Breaking Changes',
|
|
999
|
+
'',
|
|
1000
|
+
];
|
|
1001
|
+
|
|
1002
|
+
const breakingChecks = [
|
|
1003
|
+
...report.cli.checks.filter(c => c.breaking),
|
|
1004
|
+
...report.mcp.checks.filter(c => c.breaking),
|
|
1005
|
+
...report.hooks.checks.filter(c => c.breaking),
|
|
1006
|
+
...report.api.checks.filter(c => c.breaking),
|
|
1007
|
+
];
|
|
1008
|
+
|
|
1009
|
+
if (breakingChecks.length === 0) {
|
|
1010
|
+
lines.push('No breaking changes detected.');
|
|
1011
|
+
} else {
|
|
1012
|
+
lines.push('| Category | Item | V2 Behavior | V3 Behavior |');
|
|
1013
|
+
lines.push('|----------|------|-------------|-------------|');
|
|
1014
|
+
for (const check of breakingChecks.slice(0, 50)) {
|
|
1015
|
+
lines.push(`| ${check.category.toUpperCase()} | ${check.name} | ${check.v2Behavior} | ${check.v3Behavior} |`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
lines.push('');
|
|
1020
|
+
lines.push('## Recommendations');
|
|
1021
|
+
lines.push('');
|
|
1022
|
+
for (const rec of report.recommendations) {
|
|
1023
|
+
lines.push(`- ${rec}`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
lines.push('');
|
|
1027
|
+
lines.push('## Migration Guide');
|
|
1028
|
+
lines.push('');
|
|
1029
|
+
lines.push('### CLI Migration');
|
|
1030
|
+
lines.push('');
|
|
1031
|
+
lines.push('```bash');
|
|
1032
|
+
lines.push('# V2 commands are supported via compatibility layer');
|
|
1033
|
+
lines.push('# Deprecated commands will show warnings');
|
|
1034
|
+
lines.push('');
|
|
1035
|
+
lines.push('# V2 (deprecated)');
|
|
1036
|
+
lines.push('npx claude-flow hive-mind init');
|
|
1037
|
+
lines.push('');
|
|
1038
|
+
lines.push('# V3 (recommended)');
|
|
1039
|
+
lines.push('npx @sparkleideas/cli swarm init');
|
|
1040
|
+
lines.push('```');
|
|
1041
|
+
lines.push('');
|
|
1042
|
+
lines.push('### MCP Tool Migration');
|
|
1043
|
+
lines.push('');
|
|
1044
|
+
lines.push('```typescript');
|
|
1045
|
+
lines.push('// V2 tool call');
|
|
1046
|
+
lines.push("await mcp.callTool('dispatch_agent', { type: 'coder' });");
|
|
1047
|
+
lines.push('');
|
|
1048
|
+
lines.push('// V3 tool call (direct)');
|
|
1049
|
+
lines.push("await mcp.callTool('agent/spawn', { agentType: 'coder' });");
|
|
1050
|
+
lines.push('');
|
|
1051
|
+
lines.push('// V3 with compatibility layer');
|
|
1052
|
+
lines.push("await mcp.callTool('dispatch_agent', { type: 'coder' }); // Auto-translated");
|
|
1053
|
+
lines.push('```');
|
|
1054
|
+
lines.push('');
|
|
1055
|
+
lines.push('### API Migration');
|
|
1056
|
+
lines.push('');
|
|
1057
|
+
lines.push('```typescript');
|
|
1058
|
+
lines.push("// V2 imports");
|
|
1059
|
+
lines.push("import { HiveMind } from 'claude-flow/hive-mind';");
|
|
1060
|
+
lines.push("import { SwarmCoordinator } from 'claude-flow/swarm';");
|
|
1061
|
+
lines.push('');
|
|
1062
|
+
lines.push("// V3 imports (using aliases)");
|
|
1063
|
+
lines.push("import { UnifiedSwarmCoordinator as HiveMind } from '@sparkleideas/swarm';");
|
|
1064
|
+
lines.push("import { UnifiedSwarmCoordinator as SwarmCoordinator } from '@sparkleideas/swarm';");
|
|
1065
|
+
lines.push('```');
|
|
1066
|
+
lines.push('');
|
|
1067
|
+
lines.push('---');
|
|
1068
|
+
lines.push('');
|
|
1069
|
+
lines.push('*Report generated by V2CompatibilityValidator*');
|
|
1070
|
+
|
|
1071
|
+
return lines.join('\n');
|
|
1072
|
+
}
|