@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
977 lines • 41.3 kB
JavaScript
/**
* Parameter Validation Utilities
* Provides consistent parameter validation across all tool interfaces
*/
import { VIDEO_ERROR_CODES } from "../constants/videoErrors.js";
import { SYSTEM_LIMITS } from "../core/constants.js";
import { ErrorFactory } from "./errorHandling.js";
import { isNonNullObject } from "./typeUtils.js";
// ============================================================================
// VALIDATION ERROR TYPES
// ============================================================================
/**
* Custom error class for parameter validation failures
* Provides detailed information about validation errors including field context and suggestions
*/
export class ValidationError extends Error {
field;
code;
suggestions;
/**
* Creates a new ValidationError
* @param message - Human-readable error message
* @param field - Name of the field that failed validation (optional)
* @param code - Error code for programmatic handling (optional)
* @param suggestions - Array of suggested fixes (optional)
*/
constructor(message, field, code, suggestions) {
super(message);
this.field = field;
this.code = code;
this.suggestions = suggestions;
this.name = "ValidationError";
}
}
// ============================================================================
// BASIC PARAMETER VALIDATORS
// ============================================================================
/**
* Validate that a string parameter is present and non-empty
*/
export function validateRequiredString(value, fieldName, minLength = 1) {
if (value === undefined || value === null) {
return new ValidationError(`${fieldName} is required`, fieldName, "REQUIRED_FIELD", [`Provide a valid ${fieldName.toLowerCase()}`]);
}
if (typeof value !== "string") {
return new ValidationError(`${fieldName} must be a string, received ${typeof value}`, fieldName, "INVALID_TYPE", [`Convert ${fieldName.toLowerCase()} to string format`]);
}
if (value.trim().length < minLength) {
return new ValidationError(`${fieldName} must be at least ${minLength} character${minLength > 1 ? "s" : ""} long`, fieldName, "MIN_LENGTH", [`Provide a meaningful ${fieldName.toLowerCase()}`]);
}
return null;
}
/**
* Validate that a number parameter is within acceptable range
*/
export function validateNumberRange(value, fieldName, min, max, required = false) {
if (value === undefined || value === null) {
if (required) {
return new ValidationError(`${fieldName} is required`, fieldName, "REQUIRED_FIELD", [`Provide a number between ${min} and ${max}`]);
}
return null; // Optional field
}
if (typeof value !== "number" || isNaN(value)) {
return new ValidationError(`${fieldName} must be a valid number, received ${typeof value}`, fieldName, "INVALID_TYPE", [`Provide a number between ${min} and ${max}`]);
}
if (value < min || value > max) {
return new ValidationError(`${fieldName} must be between ${min} and ${max}, received ${value}`, fieldName, "OUT_OF_RANGE", [`Use a value between ${min} and ${max}`]);
}
return null;
}
/**
* Validate that a function parameter is async and has correct signature
*/
export function validateAsyncFunction(value, fieldName, expectedParams = []) {
if (typeof value !== "function") {
return new ValidationError(`${fieldName} must be a function, received ${typeof value}`, fieldName, "INVALID_TYPE", [
"Provide an async function",
`Expected signature: async (${expectedParams.join(", ")}) => Promise<unknown>`,
]);
}
// Check if function appears to be async
const funcStr = value.toString();
const isAsync = funcStr.includes("async") || funcStr.includes("Promise");
if (!isAsync) {
return new ValidationError(`${fieldName} must be an async function that returns a Promise`, fieldName, "NOT_ASYNC", [
"Add 'async' keyword to function declaration",
"Return a Promise from the function",
`Example: async (${expectedParams.join(", ")}) => { return result; }`,
]);
}
return null;
}
/**
* Validate object structure with required properties
*/
export function validateObjectStructure(value, fieldName, requiredProperties, optionalProperties = []) {
if (!isNonNullObject(value)) {
return new ValidationError(`${fieldName} must be an object, received ${typeof value}`, fieldName, "INVALID_TYPE", [
`Provide an object with properties: ${requiredProperties.join(", ")}`,
...(optionalProperties.length > 0
? [`Optional properties: ${optionalProperties.join(", ")}`]
: []),
]);
}
const obj = value;
const missingProps = requiredProperties.filter((prop) => !(prop in obj));
if (missingProps.length > 0) {
return new ValidationError(`${fieldName} is missing required properties: ${missingProps.join(", ")}`, fieldName, "MISSING_PROPERTIES", [`Add missing properties: ${missingProps.join(", ")}`]);
}
return null;
}
// ============================================================================
// TOOL-SPECIFIC VALIDATORS
// ============================================================================
/**
* Validate tool name according to naming conventions
*/
export function validateToolName(name) {
const error = validateRequiredString(name, "Tool name", 1);
if (error) {
return error;
}
const toolName = name;
// Check naming conventions
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(toolName)) {
return new ValidationError("Tool name must start with a letter and contain only letters, numbers, underscores, and hyphens", "name", "INVALID_FORMAT", [
"Use alphanumeric characters, underscores, and hyphens only",
"Start with a letter",
"Examples: 'calculateSum', 'data_processor', 'api-client'",
]);
}
if (toolName.length > 64) {
return new ValidationError(`Tool name too long: ${toolName.length} characters (max: 64)`, "name", "MAX_LENGTH", ["Use a shorter, more concise name"]);
}
// Reserved names check
const reservedNames = ["execute", "validate", "setup", "init", "config"];
if (reservedNames.includes(toolName.toLowerCase())) {
return new ValidationError(`Tool name '${toolName}' is reserved`, "name", "RESERVED_NAME", ["Choose a different name", "Add a prefix or suffix to make it unique"]);
}
return null;
}
/**
* Validate tool description for clarity and usefulness
*/
export function validateToolDescription(description) {
const error = validateRequiredString(description, "Tool description", 10);
if (error) {
return error;
}
const desc = description;
if (desc.length > 500) {
return new ValidationError(`Tool description too long: ${desc.length} characters (max: 500)`, "description", "MAX_LENGTH", ["Keep description concise and focused", "Use under 500 characters"]);
}
// Check for meaningful content
const meaningfulWords = desc.split(/\s+/).filter((word) => word.length > 2);
if (meaningfulWords.length < 3) {
return new ValidationError("Tool description should be more descriptive", "description", "TOO_BRIEF", [
"Explain what the tool does",
"Include expected parameters",
"Describe the return value",
]);
}
return null;
}
/**
* Validate MCP tool structure comprehensively
*/
export function validateMCPTool(tool) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!isNonNullObject(tool)) {
errors.push(new ValidationError("Tool must be an object", "tool", "INVALID_TYPE", [
"Provide a valid tool object with name, description, and execute properties",
]));
return { isValid: false, errors, warnings, suggestions };
}
const mcpTool = tool;
// Validate name
const nameError = validateToolName(mcpTool.name);
if (nameError) {
errors.push(nameError);
}
// Validate description
const descError = validateToolDescription(mcpTool.description);
if (descError) {
errors.push(descError);
}
// Validate execute function
const execError = validateAsyncFunction(mcpTool.execute, "execute function", [
"params",
"context",
]);
if (execError) {
errors.push(execError);
}
// Simplified validation - just check if execute is a function
if (mcpTool.execute && typeof mcpTool.execute !== "function") {
errors.push(new ValidationError("Execute must be a function", "execute", "INVALID_TYPE", ["Provide a function for the execute property"]));
}
// Check optional properties
if (mcpTool.inputSchema && !isNonNullObject(mcpTool.inputSchema)) {
warnings.push("inputSchema should be an object if provided");
suggestions.push("Provide a valid JSON schema or Zod schema for input validation");
}
if (mcpTool.outputSchema && !isNonNullObject(mcpTool.outputSchema)) {
warnings.push("outputSchema should be an object if provided");
suggestions.push("Provide a valid JSON schema or Zod schema for output validation");
}
return {
isValid: errors.length === 0,
errors,
warnings,
suggestions,
};
}
// ============================================================================
// OPTIONS VALIDATORS
// ============================================================================
/**
* Validate text generation options
*/
export function validateTextGenerationOptions(options) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!isNonNullObject(options)) {
errors.push(new ValidationError("Options must be an object", "options", "INVALID_TYPE"));
return { isValid: false, errors, warnings, suggestions };
}
const opts = options;
// Modality dispatch (output.mode === 'video' | 'avatar' | 'music') carries
// its own typed prompt inside `output.{video|avatar|music}`. The textual
// prompt is irrelevant for these modes, so skip the prompt-required check.
// Same exemption applies for STT-driven flows where `input.text` is
// synthesized from transcription downstream.
const outputMode = opts.output?.mode;
const isMediaModalityMode = outputMode === "video" || outputMode === "avatar" || outputMode === "music";
const hasSttAudio = !!opts.stt?.audio;
// Validate prompt (skipped for modality dispatch + STT-driven flows)
if (!isMediaModalityMode && !hasSttAudio) {
const promptError = validateRequiredString(opts.prompt, "prompt", 1);
if (promptError) {
errors.push(promptError);
}
}
if (opts.prompt && opts.prompt.length > SYSTEM_LIMITS.MAX_PROMPT_LENGTH) {
errors.push(new ValidationError(`Prompt too large: ${opts.prompt.length} characters (max: ${SYSTEM_LIMITS.MAX_PROMPT_LENGTH})`, "prompt", "MAX_LENGTH", [
"Break prompt into smaller chunks",
"Use summarization for long content",
"Consider using streaming for large inputs",
]));
}
// Validate temperature
const tempError = validateNumberRange(opts.temperature, "temperature", 0, 2);
if (tempError) {
errors.push(tempError);
}
// Validate maxTokens
const tokensError = validateNumberRange(opts.maxTokens, "maxTokens", 1, 128000);
if (tokensError) {
errors.push(tokensError);
}
// Validate timeout
if (opts.timeout !== undefined) {
if (typeof opts.timeout === "string") {
// Parse string timeouts like "30s", "2m", "1h"
if (!/^\d+[smh]?$/.test(opts.timeout)) {
errors.push(new ValidationError("Invalid timeout format. Use number (ms) or string like '30s', '2m', '1h'", "timeout", "INVALID_FORMAT", ["Use format: 30000 (ms), '30s', '2m', or '1h'"]));
}
}
else if (typeof opts.timeout === "number") {
if (opts.timeout < 1000 || opts.timeout > 600000) {
warnings.push("Timeout outside recommended range (1s - 10m)");
suggestions.push("Use timeout between 1000ms (1s) and 600000ms (10m)");
}
}
else {
errors.push(new ValidationError("Timeout must be a number (ms) or string", "timeout", "INVALID_TYPE"));
}
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
/**
* Validate stream options
*/
export function validateStreamOptions(options) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!isNonNullObject(options)) {
errors.push(new ValidationError("Options must be an object", "options", "INVALID_TYPE"));
return { isValid: false, errors, warnings, suggestions };
}
const opts = options;
// Validate input
if (!opts.input || !isNonNullObject(opts.input)) {
errors.push(new ValidationError("input is required and must be an object with text property", "input", "REQUIRED_FIELD", ["Provide input: { text: 'your prompt here' }"]));
}
else {
const inputError = validateRequiredString(opts.input.text, "input.text", 1);
if (inputError) {
errors.push(inputError);
}
}
// Validate temperature
const tempError = validateNumberRange(opts.temperature, "temperature", 0, 2);
if (tempError) {
errors.push(tempError);
}
// Validate maxTokens
const tokensError = validateNumberRange(opts.maxTokens, "maxTokens", 1, 128000);
if (tokensError) {
errors.push(tokensError);
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
/**
* Validate generate options (unified interface)
*/
export function validateGenerateOptions(options) {
const errors = [];
const warnings = [];
const suggestions = [];
if (!isNonNullObject(options)) {
errors.push(new ValidationError("Options must be an object", "options", "INVALID_TYPE"));
return { isValid: false, errors, warnings, suggestions };
}
const opts = options;
// Validate input
if (!opts.input || !isNonNullObject(opts.input)) {
errors.push(new ValidationError("input is required and must be an object with text property", "input", "REQUIRED_FIELD", ["Provide input: { text: 'your prompt here' }"]));
}
else {
const inputError = validateRequiredString(opts.input.text, "input.text", 1);
if (inputError) {
errors.push(inputError);
}
}
// Common validation for temperature and maxTokens
const tempError = validateNumberRange(opts.temperature, "temperature", 0, 2);
if (tempError) {
errors.push(tempError);
}
const tokensError = validateNumberRange(opts.maxTokens, "maxTokens", 1, 128000);
if (tokensError) {
errors.push(tokensError);
}
// Validate factory config if present
if (opts.factoryConfig && !isNonNullObject(opts.factoryConfig)) {
warnings.push("factoryConfig should be an object if provided");
suggestions.push("Provide valid factory configuration or remove the property");
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
// ============================================================================
// PARAMETER TRANSFORMATION VALIDATORS
// ============================================================================
/**
* Validate tool execution parameters
*/
export function validateToolExecutionParams(toolName, params, expectedSchema) {
const errors = [];
const warnings = [];
const suggestions = [];
// Basic parameter validation
if (params !== undefined && params !== null && !isNonNullObject(params)) {
errors.push(new ValidationError(`Parameters for tool '${toolName}' must be an object`, "params", "INVALID_TYPE", ["Provide parameters as an object: { key: value, ... }"]));
return { isValid: false, errors, warnings, suggestions };
}
// Schema validation (if provided)
if (expectedSchema && params) {
try {
// This is a placeholder for actual schema validation
// In practice, you would use Zod or JSON schema validation here
warnings.push("Schema validation not yet implemented");
suggestions.push("Implement Zod schema validation for tool parameters");
}
catch (error) {
errors.push(new ValidationError(`Parameter validation failed: ${error instanceof Error ? error.message : String(error)}`, "params", "SCHEMA_VALIDATION", ["Check parameter format against tool schema"]));
}
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
// ============================================================================
// BATCH VALIDATION UTILITIES
// ============================================================================
/**
* Validate multiple tools at once
*/
export function validateToolBatch(tools) {
const validTools = [];
const invalidTools = [];
const results = {};
for (const [name, tool] of Object.entries(tools)) {
const nameValidation = validateToolName(name);
const toolValidation = validateMCPTool(tool);
const combinedResult = {
isValid: !nameValidation && toolValidation.isValid,
errors: nameValidation
? [nameValidation, ...toolValidation.errors]
: toolValidation.errors,
warnings: toolValidation.warnings,
suggestions: toolValidation.suggestions,
};
results[name] = combinedResult;
if (combinedResult.isValid) {
validTools.push(name);
}
else {
invalidTools.push(name);
}
}
return {
isValid: invalidTools.length === 0,
validTools,
invalidTools,
results,
};
}
// ============================================================================
// VIDEO GENERATION VALIDATORS
// ============================================================================
/**
* Convert a NeuroLinkError to a ValidationError shape
* Used to maintain consistent error types in validation results
*/
function toValidationError(error) {
// Field can be on error directly or in context (from ErrorFactory methods)
const field = error.field ??
error.context?.field;
return new ValidationError(error.message, field, error.code, error.suggestions);
}
/**
* Valid video generation options
*/
const VALID_VIDEO_RESOLUTIONS = ["720p", "1080p"];
const VALID_VIDEO_LENGTHS = [4, 6, 8];
const VALID_VIDEO_ASPECT_RATIOS = ["9:16", "16:9"];
const MAX_VIDEO_PROMPT_LENGTH = 500;
const MAX_VIDEO_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
/**
* Validate video output options (resolution, length, aspect ratio, audio)
*
* @param options - VideoOutputOptions to validate
* @returns NeuroLinkError if invalid, null if valid
*
* @example
* ```typescript
* const error = validateVideoOutputOptions({ resolution: "4K", length: 10 });
* // error.code === "INVALID_VIDEO_RESOLUTION"
* ```
*/
export function validateVideoOutputOptions(options) {
// Validate resolution
if (options.resolution &&
!VALID_VIDEO_RESOLUTIONS.includes(options.resolution)) {
return ErrorFactory.invalidVideoResolution(options.resolution);
}
// Validate length
if (options.length !== undefined &&
!VALID_VIDEO_LENGTHS.includes(options.length)) {
return ErrorFactory.invalidVideoLength(options.length);
}
// Validate aspect ratio
if (options.aspectRatio &&
!VALID_VIDEO_ASPECT_RATIOS.includes(options.aspectRatio)) {
return ErrorFactory.invalidVideoAspectRatio(options.aspectRatio);
}
// Validate audio (must be boolean if provided)
if (options.audio !== undefined && typeof options.audio !== "boolean") {
return ErrorFactory.invalidVideoAudio(options.audio);
}
return null;
}
/**
* Validate image input for video generation
*
* Checks image format (magic bytes) and size constraints.
* Supports JPEG, PNG, and WebP formats.
*
* @param image - Image buffer to validate
* @param maxSize - Maximum allowed size in bytes (default: 10MB)
* @returns NeuroLinkError if invalid, null if valid
*
* @example
* ```typescript
* const imageBuffer = readFileSync("product.jpg");
* const error = validateImageForVideo(imageBuffer);
* if (error) throw error;
* ```
*/
export function validateImageForVideo(image, maxSize = MAX_VIDEO_IMAGE_SIZE) {
// Handle null/undefined
if (image === null || image === undefined) {
return ErrorFactory.invalidImageType();
}
// If string (URL or path), skip detailed validation
if (typeof image === "string") {
// Basic URL/path validation
if (image.trim().length === 0) {
return ErrorFactory.emptyImagePath();
}
return null;
}
// Ensure it's a Buffer
if (!Buffer.isBuffer(image)) {
return ErrorFactory.invalidImageType();
}
// Check size
if (image.length > maxSize) {
const sizeMB = (image.length / 1024 / 1024).toFixed(2);
const maxMB = (maxSize / 1024 / 1024).toFixed(0);
return ErrorFactory.imageTooLarge(sizeMB, maxMB);
}
// Check minimum size (at least a few bytes for magic number detection)
if (image.length < 8) {
return ErrorFactory.imageTooSmall();
}
// Check magic bytes for supported formats
const isJPEG = image[0] === 0xff && image[1] === 0xd8 && image[2] === 0xff;
const isPNG = image[0] === 0x89 &&
image[1] === 0x50 &&
image[2] === 0x4e &&
image[3] === 0x47;
// WebP requires both RIFF header AND WEBP signature
// Check for WebP: RIFF at bytes 0-3 AND "WEBP" at bytes 8-11
const isWebP = image.length >= 12 &&
image[0] === 0x52 &&
image[1] === 0x49 &&
image[2] === 0x46 &&
image[3] === 0x46 && // RIFF header
image[8] === 0x57 &&
image[9] === 0x45 &&
image[10] === 0x42 &&
image[11] === 0x50; // WEBP signature
const isValidFormat = isJPEG || isPNG || isWebP;
if (!isValidFormat) {
return ErrorFactory.invalidImageFormat();
}
return null;
}
/**
* Validate complete video generation input
*
* Validates all requirements for video generation:
* - output.mode must be "video"
* - Must have exactly one input image
* - Prompt must be within length limits
* - Video output options must be valid
*
* @param options - GenerateOptions to validate for video generation
* @returns EnhancedValidationResult with errors, warnings, and suggestions
*
* @example
* ```typescript
* const validation = validateVideoGenerationInput({
* input: { text: "Product showcase video", images: [imageBuffer] },
* output: { mode: "video", video: { resolution: "1080p" } }
* });
* if (!validation.isValid) {
* console.error(validation.errors);
* }
* ```
*/
export function validateVideoGenerationInput(options) {
const errors = [];
const warnings = [];
const suggestions = [];
// Must have video mode
if (options.output?.mode !== "video") {
errors.push(toValidationError(ErrorFactory.invalidVideoMode()));
}
// Must have at least one image
if (!options.input?.images || options.input.images.length === 0) {
errors.push(toValidationError(ErrorFactory.missingVideoImage()));
}
else if (options.input.images.length > 1) {
// Warn if multiple images provided - only first will be used
warnings.push("Only the first image will be used for video generation. Additional images will be ignored.");
suggestions.push("Provide a single image for video generation");
}
// Validate the first image if present
if (options.input?.images && options.input.images.length > 0) {
const firstImage = options.input.images[0];
// Handle ImageWithAltText type
const imageData = typeof firstImage === "object" && "data" in firstImage
? firstImage.data
: firstImage;
// Skip validation for URL/path strings, validate Buffers
if (typeof imageData !== "string") {
const imageError = validateImageForVideo(imageData);
if (imageError) {
errors.push(toValidationError(imageError));
}
}
}
// Validate prompt/text - trim once for consistency
const trimmedPrompt = options.input?.text?.trim() || "";
if (trimmedPrompt.length === 0) {
errors.push(toValidationError(ErrorFactory.emptyVideoPrompt()));
}
else if (trimmedPrompt.length > MAX_VIDEO_PROMPT_LENGTH) {
errors.push(toValidationError(ErrorFactory.videoPromptTooLong(trimmedPrompt.length, MAX_VIDEO_PROMPT_LENGTH)));
}
// Validate video output options if provided
if (options.output?.video) {
const videoError = validateVideoOutputOptions(options.output.video);
if (videoError) {
errors.push(toValidationError(videoError));
}
}
// Add helpful suggestions
if (errors.length === 0 && warnings.length === 0) {
suggestions.push("Video generation takes 60-180 seconds. Consider setting a longer timeout.");
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
// ============================================================================
// DIRECTOR MODE VALIDATION
// ============================================================================
const MIN_DIRECTOR_SEGMENTS = 2;
const MAX_DIRECTOR_SEGMENTS = 10;
const VALID_TRANSITION_DURATIONS = [4, 6, 8];
/**
* Validate Director Mode input: segments, transition prompts, and durations.
*
* @param options - GenerateOptions with input.segments and output.director
* @returns EnhancedValidationResult with errors, warnings, and suggestions
*/
export function validateDirectorModeInput(options) {
const errors = [];
const warnings = [];
const suggestions = [];
const segments = options.input?.segments;
if (!segments || !Array.isArray(segments)) {
errors.push(new ValidationError("Director Mode requires an input.segments array", "input.segments", VIDEO_ERROR_CODES.DIRECTOR_SEGMENT_MISMATCH));
return { isValid: false, errors, warnings, suggestions };
}
if (segments.length < MIN_DIRECTOR_SEGMENTS ||
segments.length > MAX_DIRECTOR_SEGMENTS) {
errors.push(new ValidationError(`Director Mode requires ${MIN_DIRECTOR_SEGMENTS}-${MAX_DIRECTOR_SEGMENTS} segments, got ${segments.length}`, "input.segments", segments.length > MAX_DIRECTOR_SEGMENTS
? VIDEO_ERROR_CODES.DIRECTOR_SEGMENT_LIMIT_EXCEEDED
: VIDEO_ERROR_CODES.DIRECTOR_SEGMENT_MISMATCH));
return { isValid: false, errors, warnings, suggestions };
}
// Validate each segment
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (!seg || typeof seg !== "object") {
errors.push(new ValidationError(`Segment ${i} must be an object with prompt and image`, `input.segments[${i}]`, VIDEO_ERROR_CODES.DIRECTOR_SEGMENT_MISMATCH));
continue;
}
if (!seg.prompt ||
typeof seg.prompt !== "string" ||
seg.prompt.trim().length === 0) {
errors.push(new ValidationError(`Segment ${i} requires a non-empty prompt`, `input.segments[${i}].prompt`, VIDEO_ERROR_CODES.DIRECTOR_SEGMENT_MISMATCH));
}
if (seg.image === undefined || seg.image === null) {
errors.push(new ValidationError(`Segment ${i} requires a valid image (Buffer, URL, path, or ImageWithAltText)`, `input.segments[${i}].image`, VIDEO_ERROR_CODES.DIRECTOR_SEGMENT_MISMATCH));
}
}
// Validate director options
const director = options.output?.director;
const expectedTransitions = segments.length - 1;
if (director?.transitionPrompts) {
if (director.transitionPrompts.length !== expectedTransitions) {
errors.push(new ValidationError(`Expected ${expectedTransitions} transition prompts, got ${director.transitionPrompts.length}`, "output.director.transitionPrompts", VIDEO_ERROR_CODES.DIRECTOR_SEGMENT_MISMATCH));
}
}
if (director?.transitionDurations) {
if (director.transitionDurations.length !== expectedTransitions) {
errors.push(new ValidationError(`Expected ${expectedTransitions} transition durations, got ${director.transitionDurations.length}`, "output.director.transitionDurations", VIDEO_ERROR_CODES.DIRECTOR_SEGMENT_MISMATCH));
}
else {
for (let i = 0; i < director.transitionDurations.length; i++) {
const d = director.transitionDurations[i];
if (!VALID_TRANSITION_DURATIONS.includes(d)) {
errors.push(new ValidationError(`Invalid transition duration at index ${i}: ${d}. Use 4, 6, or 8`, `output.director.transitionDurations[${i}]`, VIDEO_ERROR_CODES.DIRECTOR_INVALID_TRANSITION_DURATION));
}
}
}
}
// Validate video output options if provided
if (options.output?.video) {
const videoError = validateVideoOutputOptions(options.output.video);
if (videoError) {
errors.push(toValidationError(videoError));
}
}
if (errors.length === 0) {
const totalCalls = segments.length + expectedTransitions;
suggestions.push(`Director Mode will make ${totalCalls} API calls (${segments.length} clips + ${expectedTransitions} transitions). Ensure adequate timeout.`);
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
// ============================================================================
// PPT VALIDATION (Presentation Generation)
// ============================================================================
/**
* Valid PPT generation options
*/
const VALID_PPT_THEMES = [
"modern",
"corporate",
"creative",
"minimal",
"dark",
];
const VALID_PPT_AUDIENCES = [
"business",
"students",
"technical",
"general",
];
const VALID_PPT_TONES = [
"professional",
"casual",
"educational",
"persuasive",
];
const VALID_PPT_ASPECT_RATIOS = ["16:9", "4:3"];
const VALID_PPT_FORMATS = ["pptx"];
export const MIN_PPT_PAGES = 5;
export const MAX_PPT_PAGES = 50;
export const MIN_PPT_PROMPT_LENGTH = 10;
export const MAX_PPT_PROMPT_LENGTH = 1000;
/**
* Validate PPT output options (pages, theme, audience, tone, etc.)
*
* @param options - PPTOutputOptions to validate
* @returns NeuroLinkError if invalid, null if valid
*
* @example
* ```typescript
* const error = validatePPTOutputOptions({ pages: 100, theme: "invalid" });
* // error.code === "INVALID_PPT_PAGES"
* ```
*/
export function validatePPTOutputOptions(options) {
// Validate pages (slide count) - REQUIRED
if (options.pages === undefined || options.pages === null) {
return ErrorFactory.invalidPPTPages(undefined, "pages is required");
}
if (typeof options.pages !== "number") {
return ErrorFactory.invalidPPTPages(options.pages, "not a number");
}
if (!Number.isInteger(options.pages)) {
return ErrorFactory.invalidPPTPages(options.pages, "not an integer");
}
if (options.pages < MIN_PPT_PAGES || options.pages > MAX_PPT_PAGES) {
return ErrorFactory.invalidPPTPages(options.pages, `out of range (${MIN_PPT_PAGES}-${MAX_PPT_PAGES})`);
}
// Validate format
if (options.format !== undefined &&
!VALID_PPT_FORMATS.includes(options.format)) {
return ErrorFactory.invalidPPTFormat(options.format);
}
// Validate theme - optional (if not provided, AI will decide)
if (options.theme !== undefined &&
!VALID_PPT_THEMES.includes(options.theme)) {
return ErrorFactory.invalidPPTOutputOptions("theme", options.theme, Array.from(VALID_PPT_THEMES));
}
// Validate audience - optional (if not provided, AI will decide)
if (options.audience !== undefined &&
!VALID_PPT_AUDIENCES.includes(options.audience)) {
return ErrorFactory.invalidPPTOutputOptions("audience", options.audience, Array.from(VALID_PPT_AUDIENCES));
}
// Validate tone - optional (if not provided, AI will decide)
if (options.tone !== undefined &&
!VALID_PPT_TONES.includes(options.tone)) {
return ErrorFactory.invalidPPTOutputOptions("tone", options.tone, Array.from(VALID_PPT_TONES));
}
// Validate aspectRatio
if (options.aspectRatio !== undefined &&
!VALID_PPT_ASPECT_RATIOS.includes(options.aspectRatio)) {
return ErrorFactory.invalidPPTOutputOptions("aspectRatio", options.aspectRatio, Array.from(VALID_PPT_ASPECT_RATIOS));
}
// Validate generateAIImages (must be boolean if provided)
if (options.generateAIImages !== undefined &&
typeof options.generateAIImages !== "boolean") {
return ErrorFactory.invalidPPTOutputOptions("generateAIImages", options.generateAIImages, ["true", "false"]);
}
// Validate logoPath (string path, Buffer, or ImageWithAltText)
if (options.logoPath !== undefined) {
if (typeof options.logoPath === "string") {
if (options.logoPath.trim().length === 0) {
return ErrorFactory.invalidPPTLogoPath(options.logoPath, "empty string");
}
}
else if (Buffer.isBuffer(options.logoPath)) {
// ok
}
else if (typeof options.logoPath === "object" &&
"data" in options.logoPath) {
const data = options.logoPath.data;
if (typeof data === "string") {
if (data.trim().length === 0) {
return ErrorFactory.invalidPPTLogoPath(options.logoPath, "empty string");
}
}
else if (!Buffer.isBuffer(data)) {
return ErrorFactory.invalidPPTLogoPath(options.logoPath, "invalid data type");
}
}
else {
return ErrorFactory.invalidPPTLogoPath(options.logoPath, "invalid type");
}
}
// Validate outputPath (must be non-empty string if provided)
if (options.outputPath !== undefined) {
if (typeof options.outputPath !== "string") {
return ErrorFactory.invalidPPTOutputPath(options.outputPath, "not a string");
}
if (options.outputPath.trim().length === 0) {
return ErrorFactory.invalidPPTOutputPath(options.outputPath, "empty string");
}
}
return null;
}
/**
* Validate PPT provider (supports vertex, openai, azure, anthropic, google-ai, bedrock)
*
* @param provider - Provider name to validate
* @returns NeuroLinkError if invalid, null if valid
*
* @example
* ```typescript
* const error = validatePPTProvider("unsupported-provider");
* // error.code === "INVALID_PPT_PROVIDER"
* ```
*/
export function validatePPTProvider(provider) {
// PPT generation supported providers (subset of all AIProviderName values)
// Supports major LLM providers with structured output capabilities
const validProviders = [
"vertex",
"openai",
"azure",
"anthropic",
"google-ai",
"bedrock",
];
// Convert enum or string to lowercase string for comparison
const providerString = String(provider).toLowerCase();
if (!validProviders.includes(providerString)) {
return ErrorFactory.invalidPPTProvider(provider);
}
return null;
}
/**
* Validate complete PPT generation input
*
* Validates all requirements for presentation generation:
* - output.mode must be "ppt"
* - Prompt must be within length limits
* - PPT output options must be valid
*
* @param options - GenerateOptions to validate for PPT generation
* @returns EnhancedValidationResult with errors, warnings, and suggestions
*
* @example
* ```typescript
* const validation = validatePPTGenerationInput({
* input: { text: "Introducing Our New Product" },
* output: { mode: "ppt", ppt: { pages: 10, theme: "modern" } }
* });
* if (!validation.isValid) {
* console.error(validation.errors);
* }
* ```
*/
export function validatePPTGenerationInput(options) {
const errors = [];
const warnings = [];
const suggestions = [];
// Validate prompt/text - must exist first
if (!options.input?.text) {
errors.push(toValidationError(ErrorFactory.invalidPPTPrompt("input.text is required")));
// Return early since we can't validate further
return { isValid: false, errors, warnings, suggestions };
}
// Validate prompt/text - trim once for consistency
const trimmedPrompt = options.input.text.trim();
if (trimmedPrompt === "") {
errors.push(toValidationError(ErrorFactory.invalidPPTPrompt("empty prompt")));
}
else if (trimmedPrompt.length < MIN_PPT_PROMPT_LENGTH) {
errors.push(toValidationError(ErrorFactory.invalidPPTPrompt(`prompt too short (${trimmedPrompt.length} characters, minimum ${MIN_PPT_PROMPT_LENGTH} required)`)));
}
else if (trimmedPrompt.length > MAX_PPT_PROMPT_LENGTH) {
errors.push(toValidationError(ErrorFactory.invalidPPTPrompt(`prompt too long (${trimmedPrompt.length} characters, max ${MAX_PPT_PROMPT_LENGTH})`)));
}
// image PPT options if provided
if (options.input.images && options.input.images.length > 0) {
warnings.push("Images can be unused in PPT generation due to fail in quality standards and can lead to longer generation times.");
suggestions.push("Only provide high-quality, relevant images for PPT generation.");
}
// Validate provider (optional - only validate if explicitly provided)
if (options.provider !== undefined) {
const providerError = validatePPTProvider(options.provider);
if (providerError) {
errors.push(toValidationError(providerError));
}
}
// Mode is optional, but if provided must be "ppt"
if (options.output?.mode !== undefined && options.output.mode !== "ppt") {
errors.push(toValidationError(ErrorFactory.invalidPPTMode()));
}
// Validate PPT output options
if (options.output?.ppt) {
const pptError = validatePPTOutputOptions(options.output.ppt);
if (pptError) {
errors.push(toValidationError(pptError));
}
// Add specific warnings
const pages = options.output.ppt.pages;
if (pages !== undefined && pages > 30) {
warnings.push(`Generating ${pages} slides may take significant time (estimated: ${Math.ceil(pages * 3)}-${Math.ceil(pages * 5)} seconds)`);
}
if (options.output.ppt.generateAIImages === undefined ||
options.output.ppt.generateAIImages === true) {
suggestions.push("AI image generation is enabled. Each slide with images will take additional time (~2-5 seconds per image).");
}
// Add suggestion about AI selection being used for undefined values
const aiSelections = [];
if (options.output.ppt.theme === undefined) {
aiSelections.push("theme");
}
if (options.output.ppt.audience === undefined) {
aiSelections.push("audience");
}
if (options.output.ppt.tone === undefined) {
aiSelections.push("tone");
}
if (aiSelections.length > 0) {
suggestions.push(`AI will decide: ${aiSelections.join(", ")} (based on topic analysis)`);
}
}
else {
errors.push(toValidationError(ErrorFactory.missingPPTProperty("output.ppt", [
"Provide PPT generation options under output.ppt",
"pages is required, theme/audience/tone are optional (AI will decide if not specified)",
])));
}
// Add helpful suggestions
if (errors.length === 0 && warnings.length === 0) {
suggestions.push("PPT generation typically takes 30-120 seconds depending on slide count and image generation.");
}
return { isValid: errors.length === 0, errors, warnings, suggestions };
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Create a validation error summary for logging
*/
export function createValidationSummary(result) {
const parts = [];
if (result.errors.length > 0) {
parts.push(`Errors: ${result.errors.map((e) => e.message).join("; ")}`);
}
if (result.warnings.length > 0) {
parts.push(`Warnings: ${result.warnings.join("; ")}`);
}
if (result.suggestions.length > 0) {
parts.push(`Suggestions: ${result.suggestions.join("; ")}`);
}
return parts.join(" | ");
}
/**
* Check if validation result has only warnings (no errors)
*/
export function hasOnlyWarnings(result) {
return result.errors.length === 0 && result.warnings.length > 0;
}
//# sourceMappingURL=parameterValidation.js.map