vcluster-yaml-mcp-server 1.0.7 → 1.0.9
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 +10 -2
- package/package.json +15 -4
- package/src/cli.js +4 -5
- package/src/schema-validator.js +51 -22
- package/src/server.js +2 -743
- package/src/tool-handlers.js +384 -0
- package/src/tool-registry.js +45 -0
- package/src/validation-rules.js +181 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure tool handler functions
|
|
3
|
+
* Each handler is a pure function: input → output (no side effects except I/O)
|
|
4
|
+
* Easy to test in isolation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { validateSnippet } from './snippet-validator.js';
|
|
8
|
+
import { extractValidationRulesFromComments } from './validation-rules.js';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// RESPONSE BUILDERS (Pure Functions)
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build success response
|
|
16
|
+
* Pure function: same input → same output
|
|
17
|
+
*/
|
|
18
|
+
export function buildSuccessResponse(text) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: 'text', text }],
|
|
21
|
+
isError: false
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build error response
|
|
27
|
+
* Pure function: same input → same output
|
|
28
|
+
*/
|
|
29
|
+
export function buildErrorResponse(text) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: 'text', text }],
|
|
32
|
+
isError: true
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// FORMATTING (Pure Functions)
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format validation result to markdown
|
|
42
|
+
* Pure function - no side effects
|
|
43
|
+
*/
|
|
44
|
+
export function formatValidationResult(result, {yaml_content, description, version}) {
|
|
45
|
+
let response = '';
|
|
46
|
+
|
|
47
|
+
if (description) {
|
|
48
|
+
response += `## ${description}\n\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (result.valid) {
|
|
52
|
+
response += `✅ **Configuration validated successfully!**\n\n`;
|
|
53
|
+
response += `Version: ${version}\n`;
|
|
54
|
+
if (result.section) {
|
|
55
|
+
response += `Section: ${result.section}\n`;
|
|
56
|
+
}
|
|
57
|
+
response += `Validation time: ${result.elapsed_ms}ms\n\n`;
|
|
58
|
+
response += `### Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\`\n`;
|
|
59
|
+
} else {
|
|
60
|
+
response += `❌ **Validation failed**\n\n`;
|
|
61
|
+
|
|
62
|
+
if (result.syntax_valid === false) {
|
|
63
|
+
response += `**Syntax Error:**\n${result.syntax_error}\n\n`;
|
|
64
|
+
} else if (result.errors && result.errors.length > 0) {
|
|
65
|
+
response += `**Validation Errors:**\n`;
|
|
66
|
+
result.errors.forEach((err, idx) => {
|
|
67
|
+
response += `${idx + 1}. **${err.path}**: ${err.message}\n`;
|
|
68
|
+
});
|
|
69
|
+
response += `\n`;
|
|
70
|
+
} else if (result.error) {
|
|
71
|
+
response += `**Error:** ${result.error}\n\n`;
|
|
72
|
+
if (result.hint) {
|
|
73
|
+
response += `**Hint:** ${result.hint}\n\n`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
response += `### Provided Configuration:\n\`\`\`yaml\n${yaml_content}\n\`\`\`\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Format versions list
|
|
84
|
+
* Pure function
|
|
85
|
+
*/
|
|
86
|
+
export function formatVersionsList(versions) {
|
|
87
|
+
const display = versions.slice(0, 20);
|
|
88
|
+
const more = versions.length > 20 ? `... and ${versions.length - 20} more\n` : '';
|
|
89
|
+
|
|
90
|
+
return `Available vCluster versions:\n\n${display.map(v => `- ${v}`).join('\n')}\n${more}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format query results
|
|
95
|
+
* Pure function
|
|
96
|
+
*/
|
|
97
|
+
export function formatQueryResults(results, { query, fileName, version, maxResults }) {
|
|
98
|
+
const limitedResults = results.slice(0, maxResults);
|
|
99
|
+
const hasMore = results.length > maxResults;
|
|
100
|
+
|
|
101
|
+
const formattedResults = limitedResults.map((item, idx) =>
|
|
102
|
+
formatMatch(item, idx, limitedResults.length)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return `Found ${results.length} match${results.length === 1 ? '' : 'es'} for "${query}" in ${fileName} (${version})\n\n` +
|
|
106
|
+
formattedResults.join('\n') +
|
|
107
|
+
(hasMore ? `\n\n... showing ${maxResults} of ${results.length} total matches` : '');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Format single match
|
|
112
|
+
* Pure function
|
|
113
|
+
*/
|
|
114
|
+
function formatMatch(item, idx, total) {
|
|
115
|
+
const prefix = `**[${idx + 1}/${total}]** \`${item.path}\``;
|
|
116
|
+
|
|
117
|
+
if (item.isLeaf) {
|
|
118
|
+
const valueStr = JSON.stringify(item.value, null, 2);
|
|
119
|
+
return `${prefix}\n\`\`\`\n${valueStr}\n\`\`\`\n`;
|
|
120
|
+
} else {
|
|
121
|
+
const keys = Object.keys(item.value || {});
|
|
122
|
+
return `${prefix}\n Contains: ${keys.join(', ')}\n`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format no-match message
|
|
128
|
+
* Pure function
|
|
129
|
+
*/
|
|
130
|
+
export function formatNoMatches({ query, fileName, version, similarPaths, yamlData }) {
|
|
131
|
+
return `No matches found for "${query}" in ${fileName} (${version}).\n\n` +
|
|
132
|
+
(similarPaths.length > 0 ? `Similar paths:\n${similarPaths.map(p => ` - ${p}`).join('\n')}\n\n` : '') +
|
|
133
|
+
`Tips:\n` +
|
|
134
|
+
` - Use dot notation: "controlPlane.ingress.enabled"\n` +
|
|
135
|
+
` - Try broader terms: "${query.split('.')[0] || query.split(/\s+/)[0]}"\n` +
|
|
136
|
+
` - Use extract-validation-rules for section details\n\n` +
|
|
137
|
+
`Top-level sections:\n${Object.keys(yamlData || {}).map(k => ` - ${k}`).join('\n')}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// TOOL HANDLERS (Each is a pure async function)
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle: create-vcluster-config
|
|
146
|
+
* Pure function except for I/O (githubClient)
|
|
147
|
+
*/
|
|
148
|
+
export async function handleCreateConfig(args, githubClient) {
|
|
149
|
+
const { yaml_content, description, version = 'main' } = args;
|
|
150
|
+
|
|
151
|
+
const schemaContent = await githubClient.getFileContent('chart/values.schema.json', version);
|
|
152
|
+
const fullSchema = JSON.parse(schemaContent);
|
|
153
|
+
|
|
154
|
+
const validationResult = validateSnippet(
|
|
155
|
+
yaml_content,
|
|
156
|
+
fullSchema,
|
|
157
|
+
version,
|
|
158
|
+
null // Auto-detect section
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const formattedResponse = formatValidationResult(validationResult, {
|
|
162
|
+
yaml_content,
|
|
163
|
+
description,
|
|
164
|
+
version
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: 'text', text: formattedResponse }],
|
|
169
|
+
isError: !validationResult.valid
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handle: list-versions
|
|
175
|
+
* Pure function except for I/O
|
|
176
|
+
*/
|
|
177
|
+
export async function handleListVersions(args, githubClient) {
|
|
178
|
+
const tags = await githubClient.getTags();
|
|
179
|
+
const versionTags = tags.filter(tag => tag.startsWith('v'));
|
|
180
|
+
const versions = ['main', ...versionTags];
|
|
181
|
+
|
|
182
|
+
const formatted = formatVersionsList(versions);
|
|
183
|
+
return buildSuccessResponse(formatted);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handle: smart-query
|
|
188
|
+
* Pure function except for I/O
|
|
189
|
+
*/
|
|
190
|
+
export async function handleSmartQuery(args, githubClient) {
|
|
191
|
+
const { query, version = 'main', file = 'chart/values.yaml' } = args;
|
|
192
|
+
|
|
193
|
+
const yamlData = await githubClient.getYamlContent(file, version);
|
|
194
|
+
const searchTerm = query.toLowerCase();
|
|
195
|
+
|
|
196
|
+
// Extract all paths (pure function)
|
|
197
|
+
const allInfo = extractYamlInfo(yamlData);
|
|
198
|
+
|
|
199
|
+
// Search (pure function)
|
|
200
|
+
const results = searchYaml(allInfo, searchTerm);
|
|
201
|
+
|
|
202
|
+
// Handle no matches
|
|
203
|
+
if (results.length === 0) {
|
|
204
|
+
const similarPaths = findSimilarPaths(allInfo, searchTerm);
|
|
205
|
+
const formatted = formatNoMatches({ query, fileName: file, version, similarPaths, yamlData });
|
|
206
|
+
return buildSuccessResponse(formatted);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Sort by relevance (pure function)
|
|
210
|
+
const sorted = sortByRelevance(results, searchTerm);
|
|
211
|
+
|
|
212
|
+
// Format results
|
|
213
|
+
const formatted = formatQueryResults(sorted, {
|
|
214
|
+
query,
|
|
215
|
+
fileName: file,
|
|
216
|
+
version,
|
|
217
|
+
maxResults: 50
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return buildSuccessResponse(formatted);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handle: extract-validation-rules
|
|
225
|
+
* Pure function except for I/O
|
|
226
|
+
*/
|
|
227
|
+
export async function handleExtractRules(args, githubClient) {
|
|
228
|
+
const { version = 'main', file = 'chart/values.yaml', section } = args;
|
|
229
|
+
|
|
230
|
+
const content = await githubClient.getFileContent(file, version);
|
|
231
|
+
const rules = extractValidationRulesFromComments(content, section);
|
|
232
|
+
|
|
233
|
+
return buildSuccessResponse(JSON.stringify(rules, null, 2));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Handle: validate-config
|
|
238
|
+
* Pure function except for I/O
|
|
239
|
+
*/
|
|
240
|
+
export async function handleValidateConfig(args, githubClient) {
|
|
241
|
+
const { version = 'main', content, file } = args;
|
|
242
|
+
|
|
243
|
+
// Get YAML content
|
|
244
|
+
let yamlContent;
|
|
245
|
+
if (content) {
|
|
246
|
+
yamlContent = content;
|
|
247
|
+
} else if (file) {
|
|
248
|
+
yamlContent = await githubClient.getFileContent(file, version);
|
|
249
|
+
} else {
|
|
250
|
+
yamlContent = await githubClient.getFileContent('chart/values.yaml', version);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Get schema
|
|
254
|
+
const schemaContent = await githubClient.getFileContent('chart/values.schema.json', version);
|
|
255
|
+
const schema = JSON.parse(schemaContent);
|
|
256
|
+
|
|
257
|
+
// Validate (pure function)
|
|
258
|
+
const result = validateSnippet(yamlContent, schema, version, null);
|
|
259
|
+
|
|
260
|
+
return buildSuccessResponse(JSON.stringify(result, null, 2));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// QUERY HELPERS (Pure Functions)
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Extract all paths and values from YAML
|
|
269
|
+
* Pure function
|
|
270
|
+
*/
|
|
271
|
+
export function extractYamlInfo(obj, path = '') {
|
|
272
|
+
const info = [];
|
|
273
|
+
|
|
274
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
275
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
276
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
277
|
+
|
|
278
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
279
|
+
info.push({ path: currentPath, key, value, isLeaf: false });
|
|
280
|
+
info.push(...extractYamlInfo(value, currentPath));
|
|
281
|
+
} else {
|
|
282
|
+
info.push({ path: currentPath, key, value, isLeaf: true });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return info;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Search YAML info for query
|
|
292
|
+
* Pure function
|
|
293
|
+
*/
|
|
294
|
+
export function searchYaml(allInfo, searchTerm) {
|
|
295
|
+
const results = [];
|
|
296
|
+
const isDotNotation = searchTerm.includes('.');
|
|
297
|
+
|
|
298
|
+
if (isDotNotation) {
|
|
299
|
+
// Exact and partial dot notation matching
|
|
300
|
+
for (const item of allInfo) {
|
|
301
|
+
const pathLower = item.path.toLowerCase();
|
|
302
|
+
|
|
303
|
+
if (pathLower === searchTerm) {
|
|
304
|
+
results.push(item);
|
|
305
|
+
} else if (pathLower.endsWith(searchTerm)) {
|
|
306
|
+
results.push(item);
|
|
307
|
+
} else if (pathLower.includes(searchTerm)) {
|
|
308
|
+
results.push(item);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Keyword-based search
|
|
313
|
+
const keywords = searchTerm.split(/\s+/);
|
|
314
|
+
|
|
315
|
+
for (const item of allInfo) {
|
|
316
|
+
const pathLower = item.path.toLowerCase();
|
|
317
|
+
const keyLower = item.key.toLowerCase();
|
|
318
|
+
const valueStr = JSON.stringify(item.value).toLowerCase();
|
|
319
|
+
|
|
320
|
+
const allKeywordsMatch = keywords.every(kw =>
|
|
321
|
+
pathLower.includes(kw) || keyLower.includes(kw) || valueStr.includes(kw)
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (allKeywordsMatch) {
|
|
325
|
+
results.push(item);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return results;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Find similar paths
|
|
335
|
+
* Pure function
|
|
336
|
+
*/
|
|
337
|
+
export function findSimilarPaths(allInfo, searchTerm) {
|
|
338
|
+
return allInfo
|
|
339
|
+
.filter(item => {
|
|
340
|
+
const pathParts = item.path.toLowerCase().split('.');
|
|
341
|
+
return pathParts.some(part => part.includes(searchTerm) || searchTerm.includes(part));
|
|
342
|
+
})
|
|
343
|
+
.slice(0, 5)
|
|
344
|
+
.map(item => item.path);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Sort results by relevance
|
|
349
|
+
* Pure function
|
|
350
|
+
*/
|
|
351
|
+
export function sortByRelevance(results, searchTerm) {
|
|
352
|
+
return [...results].sort((a, b) => {
|
|
353
|
+
const scoreA = rankResult(a, searchTerm);
|
|
354
|
+
const scoreB = rankResult(b, searchTerm);
|
|
355
|
+
return scoreB - scoreA;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Rank search result
|
|
361
|
+
* Pure function
|
|
362
|
+
*/
|
|
363
|
+
function rankResult(item, searchTerm) {
|
|
364
|
+
let score = 0;
|
|
365
|
+
const pathLower = item.path.toLowerCase();
|
|
366
|
+
const keyLower = item.key.toLowerCase();
|
|
367
|
+
|
|
368
|
+
// Exact path match (highest priority)
|
|
369
|
+
if (pathLower === searchTerm) score += 100;
|
|
370
|
+
|
|
371
|
+
// Exact key match
|
|
372
|
+
if (keyLower === searchTerm) score += 50;
|
|
373
|
+
|
|
374
|
+
// Path contains exact term
|
|
375
|
+
if (pathLower.includes(searchTerm)) score += 20;
|
|
376
|
+
|
|
377
|
+
// Key contains term
|
|
378
|
+
if (keyLower.includes(searchTerm)) score += 10;
|
|
379
|
+
|
|
380
|
+
// Leaf nodes are more relevant
|
|
381
|
+
if (item.isLeaf) score += 5;
|
|
382
|
+
|
|
383
|
+
return score;
|
|
384
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Registry (Strategy Pattern)
|
|
3
|
+
* Maps tool names to handler functions
|
|
4
|
+
* Complexity: 1 (just a lookup, no conditionals)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
handleCreateConfig,
|
|
9
|
+
handleListVersions,
|
|
10
|
+
handleSmartQuery,
|
|
11
|
+
handleExtractRules,
|
|
12
|
+
handleValidateConfig,
|
|
13
|
+
buildErrorResponse
|
|
14
|
+
} from './tool-handlers.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tool registry - Strategy pattern instead of switch statement
|
|
18
|
+
* Pure object mapping: tool name → handler function
|
|
19
|
+
*/
|
|
20
|
+
export const toolHandlers = {
|
|
21
|
+
'create-vcluster-config': handleCreateConfig,
|
|
22
|
+
'list-versions': handleListVersions,
|
|
23
|
+
'smart-query': handleSmartQuery,
|
|
24
|
+
'extract-validation-rules': handleExtractRules,
|
|
25
|
+
'validate-config': handleValidateConfig
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Execute tool handler
|
|
30
|
+
* Complexity: 2 (single if statement)
|
|
31
|
+
* Pure function except for handler execution
|
|
32
|
+
*/
|
|
33
|
+
export async function executeToolHandler(toolName, args, githubClient) {
|
|
34
|
+
const handler = toolHandlers[toolName];
|
|
35
|
+
|
|
36
|
+
if (!handler) {
|
|
37
|
+
return buildErrorResponse(`Unknown tool: ${toolName}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
return await handler(args, githubClient);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return buildErrorResponse(`Error executing ${toolName}: ${error.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Rules Extraction
|
|
3
|
+
* Pure functions for parsing YAML comments and extracting validation rules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract validation rules from YAML comments
|
|
8
|
+
* Pure function except for complex internal state management
|
|
9
|
+
* Complexity: 23 → Will refactor separately
|
|
10
|
+
*/
|
|
11
|
+
export function extractValidationRulesFromComments(yamlContent, section) {
|
|
12
|
+
const lines = yamlContent.split('\n');
|
|
13
|
+
const rules = [];
|
|
14
|
+
const enums = {};
|
|
15
|
+
const dependencies = [];
|
|
16
|
+
const defaults = {};
|
|
17
|
+
|
|
18
|
+
let currentPath = [];
|
|
19
|
+
let currentComments = [];
|
|
20
|
+
let indentStack = [0];
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
const trimmedLine = line.trim();
|
|
25
|
+
|
|
26
|
+
// Skip empty lines
|
|
27
|
+
if (!trimmedLine) {
|
|
28
|
+
currentComments = [];
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Collect comments
|
|
33
|
+
if (trimmedLine.startsWith('#')) {
|
|
34
|
+
const comment = trimmedLine.substring(1).trim();
|
|
35
|
+
if (comment && !comment.startsWith('#')) {
|
|
36
|
+
currentComments.push(comment);
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Parse YAML structure
|
|
42
|
+
const indent = line.search(/\S/);
|
|
43
|
+
const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)?$/);
|
|
44
|
+
|
|
45
|
+
if (keyMatch) {
|
|
46
|
+
const key = keyMatch[2];
|
|
47
|
+
const value = keyMatch[3];
|
|
48
|
+
|
|
49
|
+
// Update path based on indentation
|
|
50
|
+
while (indentStack.length > 1 && indent <= indentStack[indentStack.length - 1]) {
|
|
51
|
+
indentStack.pop();
|
|
52
|
+
currentPath.pop();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (indent > indentStack[indentStack.length - 1]) {
|
|
56
|
+
indentStack.push(indent);
|
|
57
|
+
} else if (indent < indentStack[indentStack.length - 1]) {
|
|
58
|
+
while (indentStack.length > 1 && indent < indentStack[indentStack.length - 1]) {
|
|
59
|
+
indentStack.pop();
|
|
60
|
+
currentPath.pop();
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
currentPath.pop();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
currentPath.push(key);
|
|
67
|
+
const fullPath = currentPath.join('.');
|
|
68
|
+
|
|
69
|
+
// Filter by section if specified
|
|
70
|
+
if (section && !fullPath.startsWith(section)) {
|
|
71
|
+
currentComments = [];
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Extract validation instructions from comments
|
|
76
|
+
if (currentComments.length > 0) {
|
|
77
|
+
const instructions = [];
|
|
78
|
+
|
|
79
|
+
for (const comment of currentComments) {
|
|
80
|
+
// Extract enum values (e.g., "Valid values: a, b, c")
|
|
81
|
+
const enumMatch = comment.match(/(?:valid values?|options?|choices?|possible values?):\s*(.+)/i);
|
|
82
|
+
if (enumMatch) {
|
|
83
|
+
const values = enumMatch[1].split(/[,;]/).map(v => v.trim()).filter(v => v);
|
|
84
|
+
enums[fullPath] = values;
|
|
85
|
+
instructions.push(`Valid values: ${values.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract required dependencies
|
|
89
|
+
if (comment.match(/requires?|depends on|needs?/i)) {
|
|
90
|
+
dependencies.push(`${fullPath}: ${comment}`);
|
|
91
|
+
instructions.push(comment);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract defaults
|
|
95
|
+
const defaultMatch = comment.match(/default(?:s)?\s*(?:is|:)?\s*(.+)/i);
|
|
96
|
+
if (defaultMatch) {
|
|
97
|
+
defaults[fullPath] = defaultMatch[1].trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Extract validation rules
|
|
101
|
+
if (comment.match(/must|should|cannot|only|at least|minimum|maximum|required/i)) {
|
|
102
|
+
instructions.push(comment);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract warnings
|
|
106
|
+
if (comment.match(/warning|note|important|deprecated/i)) {
|
|
107
|
+
instructions.push(`⚠️ ${comment}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (instructions.length > 0) {
|
|
112
|
+
rules.push({
|
|
113
|
+
path: fullPath,
|
|
114
|
+
instructions: instructions,
|
|
115
|
+
originalComments: currentComments
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
currentComments = [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Generate AI validation instructions
|
|
125
|
+
const aiInstructions = {
|
|
126
|
+
summary: `Extracted ${rules.length} validation rules from YAML comments`,
|
|
127
|
+
rules: rules,
|
|
128
|
+
enums: enums,
|
|
129
|
+
dependencies: dependencies,
|
|
130
|
+
defaults: defaults,
|
|
131
|
+
instructions: generateAiValidationInstructions(rules, enums, dependencies)
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return aiInstructions;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate AI validation instructions
|
|
139
|
+
* Pure function - formats rules into markdown
|
|
140
|
+
*/
|
|
141
|
+
function generateAiValidationInstructions(rules, enums, dependencies) {
|
|
142
|
+
let instructions = '### AI Validation Instructions\n\n';
|
|
143
|
+
instructions += 'Please validate the configuration using these rules extracted from comments:\n\n';
|
|
144
|
+
|
|
145
|
+
if (rules.length > 0) {
|
|
146
|
+
instructions += '#### Field-Specific Rules:\n';
|
|
147
|
+
rules.forEach(rule => {
|
|
148
|
+
instructions += `- **${rule.path}**:\n`;
|
|
149
|
+
rule.instructions.forEach(inst => {
|
|
150
|
+
instructions += ` - ${inst}\n`;
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
instructions += '\n';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (Object.keys(enums).length > 0) {
|
|
157
|
+
instructions += '#### Enumeration Constraints:\n';
|
|
158
|
+
instructions += 'Ensure these fields only contain the specified values:\n';
|
|
159
|
+
Object.entries(enums).forEach(([field, values]) => {
|
|
160
|
+
instructions += `- ${field}: [${values.join(', ')}]\n`;
|
|
161
|
+
});
|
|
162
|
+
instructions += '\n';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (dependencies.length > 0) {
|
|
166
|
+
instructions += '#### Dependencies to Check:\n';
|
|
167
|
+
dependencies.forEach(dep => {
|
|
168
|
+
instructions += `- ${dep}\n`;
|
|
169
|
+
});
|
|
170
|
+
instructions += '\n';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
instructions += '#### Validation Approach:\n';
|
|
174
|
+
instructions += '1. Check if all enumeration constraints are satisfied\n';
|
|
175
|
+
instructions += '2. Verify all dependency requirements are met\n';
|
|
176
|
+
instructions += '3. Validate against the specific rules for each field\n';
|
|
177
|
+
instructions += '4. Flag any deprecated fields or configurations\n';
|
|
178
|
+
instructions += '5. Provide helpful suggestions for fixing any issues found\n';
|
|
179
|
+
|
|
180
|
+
return instructions;
|
|
181
|
+
}
|