@vfarcic/dot-ai
Version:
AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance
1,061 lines (1,058 loc) • 58.6 kB
JavaScript
"use strict";
/**
* Resource Schema Parser and Validator
*
* Implements comprehensive schema parsing and validation for Kubernetes resources
* Fetches structured OpenAPI schemas from Kubernetes API server and validates manifests
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResourceRecommender = exports.ManifestValidator = exports.SchemaParser = exports.OUTPUT_PATH_QUESTION = exports.OUTPUT_FORMAT_QUESTION = void 0;
const plugin_registry_1 = require("./plugin-registry");
const pattern_vector_service_1 = require("./pattern-vector-service");
const capability_vector_service_1 = require("./capability-vector-service");
const policy_vector_service_1 = require("./policy-vector-service");
const shared_prompt_loader_1 = require("./shared-prompt-loader");
const platform_utils_1 = require("./platform-utils");
const constants_1 = require("./constants");
// PRD #343: Inline sanitization (helm-utils.ts removed)
function sanitizeShellArg(arg, fieldName = 'argument') {
if (!/^[a-zA-Z0-9\-_./:\\@]+$/.test(arg)) {
throw new Error(`Invalid characters in ${fieldName}: "${arg}". Only alphanumeric characters, dashes, underscores, dots, forward slashes, colons, and @ are allowed.`);
}
return arg;
}
function sanitizeChartInfo(chart) {
return {
repositoryName: sanitizeShellArg(chart.repositoryName, 'repository name'),
repository: sanitizeShellArg(chart.repository, 'repository URL'),
chartName: sanitizeShellArg(chart.chartName, 'chart name'),
version: chart.version
? sanitizeShellArg(chart.version, 'version')
: undefined,
};
}
/**
* Packaging questions for capability-based solutions (not Helm charts)
* These are injected programmatically after AI generates questions
*/
exports.OUTPUT_FORMAT_QUESTION = {
id: 'outputFormat',
question: 'How would you like the manifests packaged?',
type: 'select',
options: ['raw', 'helm', 'kustomize'],
placeholder: 'Select output format',
suggestedAnswer: 'kustomize',
validation: { required: true },
};
exports.OUTPUT_PATH_QUESTION = {
id: 'outputPath',
question: 'Where would you like to save the output?',
type: 'text',
placeholder: 'e.g., ./manifests or ./my-app',
suggestedAnswer: './manifests',
validation: { required: true },
};
/**
* Inject packaging questions into a QuestionGroup for capability-based solutions
* Adds outputFormat and outputPath to required questions if not already present
*/
function injectPackagingQuestions(questions) {
const hasOutputFormat = questions.required.some(q => q.id === 'outputFormat');
const hasOutputPath = questions.required.some(q => q.id === 'outputPath');
const packagingQuestions = [];
if (!hasOutputFormat) {
packagingQuestions.push({ ...exports.OUTPUT_FORMAT_QUESTION });
}
if (!hasOutputPath) {
packagingQuestions.push({ ...exports.OUTPUT_PATH_QUESTION });
}
return {
...questions,
required: [...questions.required, ...packagingQuestions],
};
}
/**
* SchemaParser converts kubectl explain output to structured ResourceSchema
*/
class SchemaParser {
/**
* Parse ResourceExplanation from discovery engine into structured schema
*/
parseResourceExplanation(explanation) {
const apiVersion = explanation.group
? `${explanation.group}/${explanation.version}`
: explanation.version;
const properties = new Map();
const required = [];
// Process all fields from the explanation
for (const field of explanation.fields) {
const parts = field.name.split('.');
const topLevelField = parts[0];
// Add to required if marked as required
if (field.required && !required.includes(topLevelField)) {
required.push(topLevelField);
}
// Create or get the top-level field
if (!properties.has(topLevelField)) {
properties.set(topLevelField, {
name: topLevelField,
type: this.normalizeType(field.type),
description: field.description,
required: field.required,
constraints: this.parseFieldConstraints(field.type, field.description),
nested: new Map(),
});
}
// Handle nested fields
if (parts.length > 1) {
this.addNestedField(properties.get(topLevelField), parts.slice(1), field);
}
}
return {
apiVersion,
kind: explanation.kind,
group: explanation.group,
version: explanation.version,
description: explanation.description,
properties,
required,
namespace: true, // Default to namespaced, could be enhanced with discovery data
};
}
/**
* Add nested field to the schema structure
*/
addNestedField(parentField, fieldParts, field) {
const currentPart = fieldParts[0];
if (!parentField.nested.has(currentPart)) {
parentField.nested.set(currentPart, {
name: `${parentField.name}.${currentPart}`,
type: this.normalizeType(field.type),
description: field.description,
required: field.required,
constraints: this.parseFieldConstraints(field.type, field.description),
nested: new Map(),
});
}
// Continue recursively if there are more field parts
if (fieldParts.length > 1) {
this.addNestedField(parentField.nested.get(currentPart), fieldParts.slice(1), field);
}
}
/**
* Normalize field types from kubectl explain output
*/
normalizeType(type) {
const lowerType = type.toLowerCase();
// Map kubectl types to standard types
const typeMap = {
object: 'object',
string: 'string',
integer: 'integer',
int32: 'integer',
int64: 'integer',
boolean: 'boolean',
array: 'array',
'[]string': 'array',
'[]object': 'array',
'map[string]string': 'object',
'map[string]object': 'object',
};
return typeMap[lowerType] || 'string';
}
/**
* Parse field constraints from description text
*/
parseFieldConstraints(type, description) {
const constraints = {};
// Extract minimum/maximum values
const minMatch = description.match(/(?:minimum|min):\s*(\d+)/i);
if (minMatch) {
constraints.minimum = parseInt(minMatch[1]);
}
const maxMatch = description.match(/(?:maximum|max):\s*(\d+)/i);
if (maxMatch) {
constraints.maximum = parseInt(maxMatch[1]);
}
// Extract enum values - Fixed: Avoid catastrophic backtracking
const enumMatch = description.match(/(possible values|valid values|values)\s*(?:are)?:\s*([^.]+)/i);
if (enumMatch) {
const values = enumMatch[2]
.split(/,|\s+and\s+/)
.map(v => v.trim())
.filter(v => v.length > 0);
constraints.enum = values;
}
// Extract default values - Fixed: Use simpler non-catastrophic patterns
let defaultMatch = description.match(/\(default:\s*([^)]+)\)/i);
if (!defaultMatch) {
defaultMatch = description.match(/defaults?\s+to\s+(\w+)/i);
}
if (!defaultMatch) {
defaultMatch = description.match(/\.\s+default:\s*(\w+)/i);
}
if (defaultMatch) {
const defaultValue = defaultMatch[1].trim();
if (type === 'integer') {
const parsed = parseInt(defaultValue);
if (!isNaN(parsed)) {
constraints.default = parsed;
}
}
else {
constraints.default = defaultValue;
}
}
// Extract string length constraints
const minLengthMatch = description.match(/min length:\s*(\d+)/i);
if (minLengthMatch) {
constraints.minLength = parseInt(minLengthMatch[1]);
}
const maxLengthMatch = description.match(/max length:\s*(\d+)/i);
if (maxLengthMatch) {
constraints.maxLength = parseInt(maxLengthMatch[1]);
}
return constraints;
}
}
exports.SchemaParser = SchemaParser;
/**
* ManifestValidator validates Kubernetes manifests using kubectl dry-run
* PRD #343: Supports plugin system for kubectl operations
*/
class ManifestValidator {
/**
* PRD #359: Uses unified plugin registry for kubectl operations
*/
constructor() {
// Plugin registry will be checked at operation time
}
/**
* Execute kubectl via plugin system
* PRD #359: ALL Kubernetes operations go through unified plugin registry
*/
async executeKubectlViaPlugin(args) {
if (!(0, plugin_registry_1.isPluginInitialized)()) {
throw new Error('Plugin system not available. ManifestValidator requires agentic-tools plugin for kubectl operations.');
}
const response = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_exec_command', { args });
if (response.success) {
if (typeof response.result === 'object' && response.result !== null) {
const result = response.result;
// Check for nested error - plugin wraps kubectl errors in { success: false, error: "..." }
if (result.success === false) {
throw new Error(result.error || result.message || 'kubectl command failed');
}
// Return only the data field - never pass JSON wrapper to consumers
if (result.data !== undefined) {
return String(result.data);
}
if (typeof result === 'string') {
return result;
}
throw new Error('Plugin returned unexpected response format - missing data field');
}
return String(response.result || '');
}
else {
throw new Error(response.error?.message || 'kubectl command failed via plugin');
}
}
/**
* Validate a manifest using kubectl dry-run
* This uses the actual Kubernetes API server validation for accuracy
* PRD #359: Routes through unified plugin registry
*/
async validateManifest(manifestPath, config) {
const errors = [];
const warnings = [];
try {
const dryRunMode = config?.dryRunMode || 'server';
// PRD #359: Read manifest content and use kubectl_apply_dryrun tool via unified registry
// File paths don't work across containers, so we pass content via plugin tool
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
const yaml = await Promise.resolve().then(() => __importStar(require('yaml')));
const manifestContent = fs.readFileSync(manifestPath, 'utf8');
if (!(0, plugin_registry_1.isPluginInitialized)()) {
throw new Error('Plugin system not available. ManifestValidator requires agentic-tools plugin for kubectl operations.');
}
// Use kubectl_apply_dryrun tool which accepts manifest content
const response = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_apply_dryrun', {
manifest: manifestContent,
dryRunMode: dryRunMode,
});
if (!response.success) {
throw new Error(response.error?.message || 'kubectl dry-run validation failed');
}
// Check for nested error
if (typeof response.result === 'object' && response.result !== null) {
const result = response.result;
if (result.success === false) {
throw new Error(result.error ||
result.message ||
'kubectl dry-run validation failed');
}
}
// If we get here, validation passed
// Add best practice warnings by parsing the manifest
const documents = yaml.parseAllDocuments(manifestContent);
// Process all documents for best practice warnings
documents.forEach(doc => {
if (doc.contents) {
this.addBestPracticeWarnings(doc.toJS(), warnings);
}
});
return {
valid: true,
errors,
warnings,
};
}
catch (error) {
// Parse kubectl error output for validation issues
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('validation failed')) {
errors.push('Kubernetes validation failed: ' + errorMessage);
}
else if (errorMessage.includes('unknown field')) {
errors.push('Unknown field in manifest: ' + errorMessage);
}
else if (errorMessage.includes('required field')) {
errors.push('Missing required field: ' + errorMessage);
}
else {
errors.push('Validation error: ' + errorMessage);
}
return {
valid: false,
errors,
warnings,
};
}
}
/**
* Add best practice warnings
*/
addBestPracticeWarnings(manifest, warnings) {
// Check for missing labels
if (!manifest.metadata?.labels) {
warnings.push('Consider adding labels to metadata for better resource organization');
}
// Check for missing namespace in namespaced resources
if (!manifest.metadata?.namespace && manifest.kind !== 'Namespace') {
warnings.push('Consider specifying a namespace for better resource isolation');
}
}
}
exports.ManifestValidator = ManifestValidator;
/**
* ResourceRecommender determines which resources best meet user needs using AI
* PRD #359: Uses unified plugin registry for kubectl operations
*/
class ResourceRecommender {
aiProvider;
patternService;
capabilityService;
policyService;
constructor(aiProvider) {
// Use provided AI provider or create from environment
this.aiProvider =
aiProvider ||
(() => {
// Lazy import to avoid circular dependencies
// eslint-disable-next-line @typescript-eslint/no-require-imports -- Dynamic require to avoid circular dependency
const { createAIProvider } = require('./ai-provider-factory');
return createAIProvider();
})();
// Initialize capability service - fail gracefully if plugin unavailable
try {
// Use environment variable for collection name (allows using test data collection)
const collectionName = process.env.QDRANT_CAPABILITIES_COLLECTION || 'capabilities';
this.capabilityService = new capability_vector_service_1.CapabilityVectorService(collectionName);
console.log(`✅ Capability service initialized (collection: ${collectionName})`);
}
catch (error) {
console.warn('⚠️ Vector service initialization failed, capabilities disabled:', error);
this.capabilityService = undefined;
}
// Initialize pattern service
try {
this.patternService = new pattern_vector_service_1.PatternVectorService('patterns');
console.log('✅ Pattern service initialized');
}
catch (error) {
console.warn('⚠️ Vector service initialization failed, patterns disabled:', error);
this.patternService = undefined;
}
// Initialize policy service
try {
this.policyService = new policy_vector_service_1.PolicyVectorService();
console.log('✅ Policy service initialized');
}
catch (error) {
console.warn('⚠️ Vector service initialization failed, policies disabled:', error);
this.policyService = undefined;
}
}
/**
* Execute kubectl via plugin system
* PRD #359: ALL Kubernetes operations go through unified plugin registry
*/
async executeKubectlViaPlugin(args) {
if (!(0, plugin_registry_1.isPluginInitialized)()) {
throw new Error('Plugin system not available. ResourceRecommender requires agentic-tools plugin for kubectl operations.');
}
const response = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_exec_command', { args });
if (response.success) {
if (typeof response.result === 'object' && response.result !== null) {
const result = response.result;
// Check for nested error - plugin wraps kubectl errors in { success: false, error: "..." }
if (result.success === false) {
throw new Error(result.error || result.message || 'kubectl command failed');
}
// Return only the data field - never pass JSON wrapper to consumers
if (result.data !== undefined) {
return String(result.data);
}
if (typeof result === 'string') {
return result;
}
throw new Error('Plugin returned unexpected response format - missing data field');
}
return String(response.result || '');
}
else {
throw new Error(response.error?.message || 'kubectl command failed via plugin');
}
}
/**
* Find the best resource solution(s) for user intent using two-phase analysis
*/
async findBestSolutions(intent, _explainResource, interaction_id) {
if (!this.aiProvider.isInitialized()) {
throw new Error(constants_1.AI_SERVICE_ERROR_TEMPLATES.PROVIDER_NOT_INITIALIZED('AI-powered resource ranking'));
}
try {
// Phase 0: Search for relevant organizational patterns
const relevantPatterns = await this.searchRelevantPatterns(intent);
// Phase 1a: Replace mass resource discovery with capability-based pre-filtering
if (!this.capabilityService) {
// Capability service not available - fail fast with clear guidance
throw new Error(`Capability service not available for intent "${intent}". Please scan your cluster first:\n` +
`Run: manageOrgData({ dataType: "capabilities", operation: "scan" })\n` +
`Note: Vector DB is required for capability-based recommendations.`);
}
let relevantCapabilities = [];
if (this.capabilityService) {
try {
relevantCapabilities =
await this.capabilityService.searchCapabilities(intent, {
limit: 50,
});
}
catch (error) {
// Capability search failed - fail fast with clear guidance
throw new Error(`Capability search failed for intent "${intent}". Please scan your cluster first:\n` +
`Run: manageOrgData({ dataType: "capabilities", operation: "scan" })\n` +
`Error: ${error}`, { cause: error });
}
}
else {
console.warn('⚠️ Capability service not available (Vector DB not reachable), proceeding without capabilities');
}
if (relevantCapabilities.length === 0) {
// Fail fast with clear user guidance if no capabilities found
throw new Error(`No capabilities found for "${intent}". Please scan your cluster first:\n` +
`Run: manageOrgData({ dataType: "capabilities", operation: "scan" })`);
}
console.log(`🎯 Found ${relevantCapabilities.length} relevant capabilities (vs 415+ mass discovery)`);
// Create normalized resource objects from capability matches
const capabilityFilteredResources = relevantCapabilities.map(cap => ({
kind: this.extractKindFromResourceName(cap.data.resourceName),
group: cap.data.group ||
this.extractGroupFromResourceName(cap.data.resourceName),
apiVersion: cap.data.apiVersion, // Use stored apiVersion from capability scan
version: cap.data.version, // Just the version part (e.g., "v1beta1")
resourceName: cap.data.resourceName,
capabilities: cap.data, // Include capability data for AI decision-making (includes namespaced, etc.)
}));
// Phase 1: Add missing pattern-suggested resources to available resources list
const enhancedResources = await this.addMissingPatternResources(capabilityFilteredResources, relevantPatterns);
// Phase 2: AI assembles and ranks complete solutions (replaces separate selection + ranking phases)
const solutionResult = await this.assembleAndRankSolutions(intent, enhancedResources, relevantPatterns, interaction_id);
// If Helm is recommended, return early - questions will be generated from Helm chart values later
if (solutionResult.helmRecommendation) {
console.log(`🎯 Helm installation recommended for "${intent}": ${solutionResult.helmRecommendation.suggestedTool}`);
return solutionResult;
}
// Phase 3: Generate questions for each capability-based solution
for (const solution of solutionResult.solutions) {
solution.questions = await this.generateQuestionsWithAI(intent, solution, _explainResource, interaction_id);
}
return solutionResult;
}
catch (error) {
throw new Error(`AI-powered resource solution analysis failed: ${error}`, { cause: error });
}
}
/**
* Phase 2: AI assembles and ranks complete solutions (replaces separate selection + ranking)
*/
async assembleAndRankSolutions(intent, availableResources, patterns, interaction_id) {
const prompt = await this.loadSolutionAssemblyPrompt(intent, availableResources, patterns);
const response = await this.aiProvider.sendMessage(prompt, 'recommend-solution-assembly', {
user_intent: intent
? `Kubernetes solution assembly for: ${intent}`
: 'Kubernetes solution assembly',
interaction_id: interaction_id || 'recommend_solution_assembly',
});
return this.parseSimpleSolutionResponse(response.content);
}
/**
* Parse AI response for simple solution structure (no schema matching needed)
*/
parseSimpleSolutionResponse(aiResponse) {
try {
// Use robust JSON extraction
const parsed = (0, platform_utils_1.extractJsonFromAIResponse)(aiResponse);
// Handle Helm recommendation case (presence of helmRecommendation means Helm is needed)
const helmRecommendation = parsed.helmRecommendation || null;
// If Helm is recommended (empty solutions + helmRecommendation present), return early
if (helmRecommendation &&
(!parsed.solutions || parsed.solutions.length === 0)) {
return {
solutions: [],
helmRecommendation,
};
}
const solutions = (parsed.solutions || []).map((solution) => {
const isDebugMode = process.env.DOT_AI_DEBUG === 'true';
if (isDebugMode) {
console.debug('DEBUG: solution object:', JSON.stringify(solution, null, 2));
}
// Convert resource references to ResourceSchema format for compatibility
const resources = (solution.resources || []).map(resource => ({
kind: resource.kind,
apiVersion: resource.apiVersion,
group: resource.group || '',
resourceName: resource.resourceName, // Preserve resourceName from AI response
description: `${resource.kind} resource from ${resource.group || 'core'} group`,
properties: new Map(),
namespace: true, // Default assumption for new architecture
}));
return {
type: solution.type,
resources,
score: solution.score,
description: solution.description,
reasons: solution.reasons || [],
questions: {
required: [],
basic: [],
advanced: [],
open: { question: '', placeholder: '' },
},
appliedPatterns: solution.appliedPatterns || [],
};
});
// Sort by score descending
const sortedSolutions = solutions.sort((a, b) => b.score - a.score);
return {
solutions: sortedSolutions,
helmRecommendation,
};
}
catch (error) {
// Enhanced error message with more context
const errorMsg = `Failed to parse AI solution response: ${error.message}`;
const contextMsg = `\nAI Response (first 500 chars): "${aiResponse.substring(0, 500)}..."`;
throw new Error(errorMsg + contextMsg, { cause: error });
}
}
/**
* Load and format solution assembly prompt from file
*/
async loadSolutionAssemblyPrompt(intent, resources, patterns) {
// Format resources for the prompt with capability information
const resourcesText = resources
.map((resource, index) => {
return `${index}: ${resource.kind.toUpperCase()}
Group: ${resource.group || 'core'}
API Version: ${resource.apiVersion || 'unknown'}
Resource Name: ${resource.resourceName}
Capabilities: ${Array.isArray(resource.capabilities.capabilities) ? resource.capabilities.capabilities.join(', ') : 'Not specified'}
Providers: ${Array.isArray(resource.capabilities.providers) ? resource.capabilities.providers.join(', ') : resource.capabilities.providers || 'kubernetes'}
Complexity: ${resource.capabilities.complexity || 'medium'}
Use Case: ${resource.capabilities.useCase || resource.capabilities.description || 'General purpose'}
Description: ${resource.capabilities.description || 'Kubernetes resource'}
Confidence: ${resource.capabilities.confidence || 1.0}`;
})
.join('\n\n');
// Format organizational patterns for AI context
const patternsContext = patterns.length > 0
? patterns
.map(pattern => `- ID: ${pattern.id}
Description: ${pattern.description}
Suggested Resources: ${pattern.suggestedResources?.join(', ') || 'Not specified'}
Rationale: ${pattern.rationale}
Triggers: ${pattern.triggers?.join(', ') || 'None'}`)
.join('\n')
: 'No organizational patterns found for this request.';
return (0, shared_prompt_loader_1.loadPrompt)('resource-selection', {
intent,
resources: resourcesText,
patterns: patternsContext,
});
}
/**
* Add pattern-suggested resources that are missing from capability search results
*/
async addMissingPatternResources(capabilityResources, patterns) {
if (!patterns.length) {
return capabilityResources;
}
// Extract all resource names already in capability results
const existingResourceNames = new Set(capabilityResources.map(r => r.resourceName));
// Collect missing pattern resources
const missingPatternResources = [];
for (const pattern of patterns) {
if (pattern.suggestedResources) {
for (const suggestedResource of pattern.suggestedResources) {
// Skip null/undefined resources
if (!suggestedResource || typeof suggestedResource !== 'string') {
continue;
}
// Convert pattern resource format to resource name (e.g., "resourcegroups.azure.upbound.io" -> resourceName)
const resourceName = suggestedResource.includes('.')
? suggestedResource
: `${suggestedResource}.core`;
// Only add if not already present in capability results
if (!existingResourceNames.has(resourceName)) {
try {
// Parse resource components
const parts = suggestedResource.split('.');
const kind = parts[0]; // Use resource name as-is: resourcegroups, servicemonitors, etc.
const group = parts.length > 1 ? parts.slice(1).join('.') : '';
missingPatternResources.push({
kind,
group,
resourceName,
capabilities: {
resourceName,
description: `Resource suggested by organizational pattern: ${pattern.description}`,
capabilities: [
`organizational pattern`,
pattern.description.toLowerCase(),
],
providers: this.inferProvidersFromResourceName(suggestedResource),
complexity: 'medium',
useCase: `Pattern-suggested resource for: ${pattern.rationale}`,
confidence: 1.0, // High confidence since it's from organizational pattern
source: 'organizational-pattern',
patternId: pattern.id,
},
});
existingResourceNames.add(resourceName);
}
catch (error) {
console.warn(`Failed to parse pattern resource ${suggestedResource}:`, error);
}
}
}
}
}
return [...capabilityResources, ...missingPatternResources];
}
/**
* Infer cloud providers from resource name
*/
inferProvidersFromResourceName(resourceName) {
if (resourceName.includes('azure'))
return ['azure'];
if (resourceName.includes('aws'))
return ['aws'];
if (resourceName.includes('gcp') || resourceName.includes('google'))
return ['gcp'];
return ['kubernetes'];
}
/**
* Extract Kubernetes kind from resource name (e.g., "sqls.devopstoolkit.live" -> "SQL")
*/
extractKindFromResourceName(resourceName) {
// For CRDs like "sqls.devopstoolkit.live", the kind is usually the singular of the plural
// For core resources like "pods", return as-is
if (!resourceName.includes('.')) {
return resourceName; // Core resources like "pods", "services"
}
// For CRDs, extract the resource part (before first dot)
const resourcePart = resourceName.split('.')[0];
// Convert plural to singular and capitalize (sqls -> SQL)
return resourcePart.toUpperCase();
}
/**
* Extract group from resource name (e.g., "sqls.devopstoolkit.live" -> "devopstoolkit.live")
*/
extractGroupFromResourceName(resourceName) {
if (!resourceName.includes('.')) {
return 'core'; // Core resources have no group
}
// Return everything after the first dot
return resourceName.substring(resourceName.indexOf('.') + 1);
}
// Note: constructApiVersionFromResourceName method removed - no longer needed
// API versions are extracted from kubectl explain schema content during manifest generation
/**
* Phase 0: Search for relevant organizational patterns using multi-concept approach
* Returns empty array if Vector DB is not available - this is completely optional
*/
async searchRelevantPatterns(intent) {
// If pattern service is not available, skip pattern search entirely
if (!this.patternService) {
console.log('📋 Pattern service unavailable, skipping pattern search - using pure AI recommendations');
return [];
}
try {
// Search patterns directly with user intent (vector search handles semantic concepts)
const patternResults = await this.patternService.searchPatterns(intent, {
limit: 5,
});
return patternResults.map(result => result.data);
}
catch (error) {
// Pattern search is non-blocking - if it fails, continue without patterns
console.warn('❌ Pattern search failed, continuing without patterns:', error);
return [];
}
}
// REMOVED: selectResourceCandidates - replaced by single-phase assembleAndRankSolutions
// REMOVED: fetchDetailedSchemas - no longer needed in single-phase architecture
/**
* Phase 2: Fetch detailed schemas for selected candidates
*/
async fetchDetailedSchemas(candidates, explainResource) {
const schemas = [];
const errors = [];
for (const resource of candidates) {
try {
const explanation = await explainResource(resource.kind);
// Parse GROUP, KIND, VERSION from kubectl explain output
const lines = explanation.split('\n');
const groupLine = lines.find((line) => line.startsWith('GROUP:'));
const kindLine = lines.find((line) => line.startsWith('KIND:'));
const versionLine = lines.find((line) => line.startsWith('VERSION:'));
const group = groupLine ? groupLine.replace('GROUP:', '').trim() : '';
const kind = kindLine
? kindLine.replace('KIND:', '').trim()
: resource.kind;
const version = versionLine
? versionLine.replace('VERSION:', '').trim()
: 'v1';
// Build apiVersion from group and version
const apiVersion = group ? `${group}/${version}` : version;
// Create a simple schema with raw explanation for AI processing
const schema = {
kind: kind,
apiVersion: apiVersion,
group: group,
description: explanation
.split('\n')
.find((line) => line.startsWith('DESCRIPTION:'))
?.replace('DESCRIPTION:', '')
.trim() || '',
properties: new Map(),
rawExplanation: explanation, // Include raw explanation for AI
};
schemas.push(schema);
}
catch (error) {
errors.push(`${resource.kind}: ${error.message}`);
}
}
if (schemas.length === 0) {
throw new Error(`Could not fetch schemas for any selected resources. Candidates: ${candidates.map(c => c.kind).join(', ')}. Errors: ${errors.join(', ')}`);
}
if (errors.length > 0) {
console.warn(`Some resources could not be analyzed: ${errors.join(', ')}`);
console.warn(`Successfully fetched schemas for: ${schemas.map(s => s.kind).join(', ')}`);
}
return schemas;
}
/**
* Discover cluster options for dynamic question generation
* PRD #343: ALL kubectl operations go through plugin
*/
async discoverClusterOptions() {
try {
// Discover namespaces via plugin
const namespacesResult = await this.executeKubectlViaPlugin([
'get',
'namespaces',
'-o',
'jsonpath={.items[*].metadata.name}',
]);
const namespaces = namespacesResult.split(/\s+/).filter(Boolean);
// Discover storage classes with default marking
let storageClasses = [];
try {
const storageResult = await this.executeKubectlViaPlugin([
'get',
'storageclass',
'-o',
'json',
]);
const storageData = JSON.parse(storageResult);
storageClasses = (storageData.items || []).map((item) => ({
name: item.metadata?.name || '',
isDefault: item.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true',
}));
}
catch {
// Storage classes might not be available in all clusters
storageClasses = [];
}
// Discover ingress classes with default marking
let ingressClasses = [];
try {
const ingressResult = await this.executeKubectlViaPlugin([
'get',
'ingressclass',
'-o',
'json',
]);
const ingressData = JSON.parse(ingressResult);
ingressClasses = (ingressData.items || []).map((item) => ({
name: item.metadata?.name || '',
isDefault: item.metadata?.annotations?.['ingressclass.kubernetes.io/is-default-class'] === 'true',
}));
}
catch {
// Ingress classes might not be available
ingressClasses = [];
}
// Get common node labels
let nodeLabels = [];
try {
const nodesResult = await this.executeKubectlViaPlugin([
'get',
'nodes',
'-o',
'json',
]);
const nodes = JSON.parse(nodesResult);
const labelSet = new Set();
nodes.items?.forEach((node) => {
Object.keys(node.metadata?.labels || {}).forEach(label => {
if (!label.startsWith('kubernetes.io/') &&
!label.startsWith('node.kubernetes.io/')) {
labelSet.add(label);
}
});
});
nodeLabels = Array.from(labelSet);
}
catch {
nodeLabels = [];
}
return {
namespaces,
storageClasses,
ingressClasses,
nodeLabels,
};
}
catch (error) {
console.warn('Failed to discover cluster options, using defaults:', error);
return {
namespaces: ['default'],
storageClasses: [],
ingressClasses: [],
nodeLabels: [],
};
}
}
/**
* Format cluster options for inclusion in prompts
*/
formatClusterOptionsText(clusterOptions) {
const formatResourceList = (items) => {
if (items.length === 0)
return 'None discovered';
return items
.map(item => (item.isDefault ? `${item.name} (default)` : item.name))
.join(', ');
};
return `Available Namespaces: ${clusterOptions.namespaces.join(', ')}
Available Storage Classes: ${formatResourceList(clusterOptions.storageClasses)}
Available Ingress Classes: ${formatResourceList(clusterOptions.ingressClasses)}
Available Node Labels: ${clusterOptions.nodeLabels.length > 0 ? clusterOptions.nodeLabels.slice(0, 10).join(', ') : 'None discovered'}`;
}
/**
* Generate contextual questions using AI based on user intent and solution resources
*/
async generateQuestionsWithAI(intent, solution, _explainResource, interaction_id) {
try {
// Discover cluster options for dynamic questions
const clusterOptions = await this.discoverClusterOptions();
// Search for relevant policy intents based on the selected resources
let relevantPolicyResults = [];
if (this.policyService) {
try {
const resourceContext = solution.resources
.map(r => `${r.kind} ${r.description}`)
.join(' ');
const policyResults = await this.policyService.searchPolicyIntents(`${intent} ${resourceContext}`, { limit: 50 });
relevantPolicyResults = policyResults.map(result => ({
policy: result.data,
score: result.score,
matchType: result.matchType,
}));
console.log(`🛡️ Found ${relevantPolicyResults.length} relevant policy intents for question generation`);
}
catch (error) {
console.warn('⚠️ Policy search failed during question generation, proceeding without policies:', error);
}
}
else {
console.log('🛡️ Policy service unavailable, skipping policy search - proceeding without policy guidance');
}
// Fetch resource schemas for each resource in the solution
const resourcesWithSchemas = await Promise.all(solution.resources.map(async (resource) => {
// Validate that resource has resourceName field for kubectl explain
if (!resource.resourceName) {
throw new Error(`Resource ${resource.kind} is missing resourceName field. This indicates a bug in solution construction.`);
}
try {
// Use resourceName for kubectl explain - this should be the plural form like 'pods', 'services', etc.
const schemaExplanation = await _explainResource(resource.resourceName);
return {
...resource,
rawExplanation: schemaExplanation,
};
}
catch (error) {
console.warn(`Failed to fetch schema for ${resource.kind}: ${error}`);
return resource;
}
}));
// Format resource details for the prompt using raw explanation when available
const resourceDetails = resourcesWithSchemas
.map(resource => {
if (resource.rawExplanation) {
// Use raw kubectl explain output for comprehensive field information
return `${resource.kind} (${resource.apiVersion}):
Description: ${resource.description}
Complete Schema Information:
${resource.rawExplanation}`;
}
else {
// Fallback to properties map if raw explanation is not available
const properties = Array.from(resource.properties.entries())
.map(([key, field]) => {
const nestedFields = Array.from(field.nested.entries())
.map(([nestedKey, nestedField]) => ` ${nestedKey}: ${nestedField.type} - ${nestedField.description}`)
.join('\n');
return ` ${key}: ${field.type} - ${field.description}${field.required ? ' (required)' : ''}${nestedFields ? '\n' + nestedFields : ''}`;
})
.join('\n');
return `${resource.kind} (${resource.apiVersion}):
Description: ${resource.description}
Required fields: ${resource.required?.join(', ') || 'none specified'}
Properties:
${properties}`;
}
})
.join('\n\n');
// Format cluster options for the prompt
const clusterOptionsText = this.formatClusterOptionsText(clusterOptions);
// Format organizational policies for AI context with relevance scores
const policyContextText = relevantPolicyResults.length > 0
? relevantPolicyResults
.map(result => `- ID: ${result.policy.id}
Description: ${result.policy.description}
Rationale: ${result.policy.rationale}
Triggers: ${result.policy.triggers?.join(', ') || 'None'}
Score: ${result.score.toFixed(3)} (${result.matchType})`)
.join('\n')
: 'No organizational policies found for this request.';
// Build source_material for capabilities (Kubernetes resource-based solutions)
const sourceMaterial = `## Source Material
You are generating questions for Kubernetes resources. The schemas below define the available configuration options.
## Resources in Solution
${resourceDetails}`;
// Generate question prompt with variables
const questionPrompt = (0, shared_prompt_loader_1.loadPrompt)('question-generation', {
intent,
solution_description: solution.description,
source_material: sourceMaterial,
cluster_options: clusterOptionsText,
policy_context: policyContextText,
});
const response = await this.aiProvider.sendMessage(questionPrompt, 'recommend-question-generation', {
user_intent: `Generate deployment questions for: ${intent}`,
interaction_id: interaction_id || 'recommend_question_generation',
});
// Use robust JSON extraction
const questions = (0, platform_utils_1.extractJsonFromAIResponse)(response.content);
// Validate the response structure
if (!questions.required ||
!questions.basic ||
!questions.advanced ||
!questions.open) {
throw new Error('Invalid question structure from AI');
}
// Sanitize questions: ensure suggestedAnswer passes its own validation constraints
const sanitizeQuestions = (qs) => {
for (const q of qs) {
if ((q.type === 'select' || q.type === 'multiselect') &&
q.options &&
q.options.length > 0 &&
q.suggestedAnswer !== undefined) {
if (q.type === 'select' && !q.options.includes(q.suggestedAnswer)) {
q.suggestedAnswer = q.options[0];
}
else if (q.type === 'multiselect') {
if (!Array.isArray(q.suggestedAnswer)) {
q.suggestedAnswer = [q.options[0]];
}
else {
q.suggestedAnswer = q.suggestedAnswer.filter(a => q.options.includes(a));
if (q.suggestedAnswer.length === 0) {
q.suggestedAnswer = [q.options[0]];
}
}
}
}
// Clamp number suggestedAnswer to validation.min/max bounds