unsloth/studio/frontend/src/features/recipe-studio/utils/validation.ts

338 lines
12 KiB
TypeScript

import type { NodeConfig } from "../types";
import { isValidSex, parseAgeRange, parseIntNumber, parseNumber } from "./parse";
import { VALIDATOR_OXC_CODE_LANGS, VALIDATOR_SQL_CODE_LANGS } from "./validators/code-lang";
import { isOxcCodeShape } from "./validators/oxc-code-shape";
import { isOxcValidationMode } from "./validators/oxc-mode";
const TRACE_MODES = new Set(["none", "last_message", "all_messages"]);
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: validation rules
export function getConfigErrors(config: NodeConfig | null): string[] {
if (!config) {
return [];
}
const errors: string[] = [];
if (!config.name.trim()) {
errors.push("Name is required.");
}
if (config.kind === "sampler") {
if (config.sampler_type === "category") {
const values = config.values ?? [];
if (values.length < 2) {
errors.push("Category needs at least 2 values.");
}
const weights = config.weights ?? [];
const hasWeights = weights.some((weight) => weight !== null);
if (hasWeights && weights.some((weight) => weight === null)) {
errors.push("Weights must be set for all values.");
}
for (const [condition, params] of Object.entries(
config.conditional_params ?? {},
)) {
if (!condition.trim()) {
errors.push("Category conditional rule needs condition text.");
continue;
}
const conditionalValues = (params.values ?? [])
.map((value) => value.trim())
.filter(Boolean);
if (conditionalValues.length === 0) {
errors.push(`Category conditional '${condition}' needs values.`);
continue;
}
const conditionalWeights = params.weights ?? [];
const hasConditionalWeights = conditionalWeights.some(
(weight) => weight !== null,
);
if (
hasConditionalWeights &&
(conditionalWeights.length !== conditionalValues.length ||
conditionalWeights.some((weight) => weight === null))
) {
errors.push(
`Category conditional '${condition}' weights must be set for all values.`,
);
}
}
}
if (config.sampler_type === "uniform") {
const low = parseNumber(config.low);
const high = parseNumber(config.high);
if (low === null || high === null) {
errors.push("Uniform low/high must be numbers.");
} else if (low >= high) {
errors.push("Uniform low must be < high.");
}
}
if (config.sampler_type === "gaussian") {
const mean = parseNumber(config.mean);
const std = parseNumber(config.std);
if (mean === null || std === null) {
errors.push("Gaussian mean/std must be numbers.");
} else if (std <= 0) {
errors.push("Gaussian std must be > 0.");
}
}
if (config.sampler_type === "bernoulli") {
const p = parseNumber(config.p);
if (p === null) {
errors.push("Bernoulli p must be a number.");
} else if (p < 0 || p > 1) {
errors.push("Bernoulli p must be between 0 and 1.");
}
}
if (config.sampler_type === "datetime") {
if (!config.datetime_unit) {
errors.push("Datetime unit required.");
}
if (config.datetime_start && config.datetime_end) {
const start = new Date(config.datetime_start).getTime();
const end = new Date(config.datetime_end).getTime();
if (!(Number.isFinite(start) && Number.isFinite(end))) {
errors.push("Datetime start/end must be valid.");
} else if (start >= end) {
errors.push("Datetime start must be before end.");
}
}
}
if (config.sampler_type === "timedelta") {
const min = parseNumber(config.dt_min);
const max = parseNumber(config.dt_max);
if (min === null || max === null) {
errors.push("Timedelta dt_min/dt_max must be numbers.");
} else if (min >= max) {
errors.push("Timedelta dt_min must be < dt_max.");
}
if (!config.reference_column_name?.trim()) {
errors.push("Timedelta reference datetime column required.");
}
if (!config.timedelta_unit) {
errors.push("Timedelta unit required.");
}
}
if (config.sampler_type === "subcategory" && !config.subcategory_parent) {
errors.push("Subcategory needs a parent category column.");
}
if (
config.sampler_type === "person" ||
config.sampler_type === "person_from_faker"
) {
if (config.person_sex?.trim()) {
const normalized = config.person_sex.trim();
if (!isValidSex(normalized)) {
errors.push("Person sex must be Male or Female.");
}
}
if (config.person_age_range?.trim()) {
const parsed = parseAgeRange(config.person_age_range);
if (!parsed) {
errors.push("Person age range must be like 18-70.");
}
}
}
}
if (config.kind === "llm") {
if (!config.model_alias.trim()) {
errors.push("Model alias is required.");
}
if (!config.prompt.trim()) {
errors.push("Prompt is required.");
}
if (config.llm_type === "code" && !config.code_lang) {
errors.push("Code language is required.");
}
if (config.llm_type === "structured") {
if (!config.output_format?.trim()) {
errors.push("Output format is required.");
} else {
try {
JSON.parse(config.output_format);
} catch {
errors.push("Output format must be valid JSON.");
}
}
}
if (config.llm_type === "judge") {
const scores = config.scores ?? [];
if (scores.length === 0) {
errors.push("LLM Judge needs at least one score.");
}
for (const score of scores) {
if (!score.name.trim()) {
errors.push("LLM Judge score name is required.");
}
if (!score.description.trim()) {
errors.push("LLM Judge score description is required.");
}
const options = score.options ?? [];
if (options.length === 0) {
errors.push(`LLM Judge score ${score.name || "Unnamed"} needs options.`);
}
for (const option of options) {
if (!option.value.trim() || !option.description.trim()) {
errors.push(
`LLM Judge score ${score.name || "Unnamed"} options need value + description.`,
);
break;
}
}
}
}
if (config.image_context?.enabled) {
if (!config.image_context.column_name.trim()) {
errors.push("Image context column is required.");
}
}
if (
config.with_trace &&
!TRACE_MODES.has(config.with_trace)
) {
errors.push("Trace mode must be none, last_message, or all_messages.");
}
}
if (config.kind === "expression") {
if (!config.expr.trim()) {
errors.push("Expression is required.");
}
}
if (config.kind === "tool_config") {
if (config.mcp_providers.length === 0) {
errors.push("Add at least one MCP server.");
}
const serverNames = new Set<string>();
for (const provider of config.mcp_providers) {
const name = provider.name.trim();
if (!name) {
errors.push("Each MCP server needs a name.");
continue;
}
if (serverNames.has(name)) {
errors.push(`Duplicate MCP server name: ${name}.`);
}
serverNames.add(name);
if (provider.provider_type === "stdio") {
if (!provider.command?.trim()) {
errors.push(`MCP server ${name}: command is required.`);
}
} else if (!provider.endpoint?.trim()) {
errors.push(`MCP server ${name}: endpoint is required.`);
}
}
const maxTurnsRaw = config.max_tool_call_turns?.trim();
if (
maxTurnsRaw &&
(!Number.isFinite(Number(maxTurnsRaw)) || Number(maxTurnsRaw) < 1)
) {
errors.push("Max tool call turns must be >= 1.");
}
const timeoutRaw = config.timeout_sec?.trim();
if (
timeoutRaw &&
(!Number.isFinite(Number(timeoutRaw)) || Number(timeoutRaw) <= 0)
) {
errors.push("Timeout must be > 0.");
}
}
if (config.kind === "validator") {
const targets = (config.target_columns ?? [])
.map((value) => value.trim())
.filter(Boolean);
if (targets.length === 0) {
errors.push("Target code column is required.");
}
const batch = parseIntNumber(config.batch_size);
if (batch === null || batch < 1) {
errors.push("Batch size must be an integer >= 1.");
}
if (!config.code_lang.trim()) {
errors.push("Validator code language is required.");
} else if (config.validator_type === "oxc") {
if (!VALIDATOR_OXC_CODE_LANGS.includes(config.code_lang)) {
errors.push("OXC validator code language must be javascript/typescript/jsx/tsx.");
}
if (!isOxcValidationMode(config.oxc_validation_mode)) {
errors.push("OXC validation mode must be syntax, lint, or syntax+lint.");
}
if (!isOxcCodeShape(config.oxc_code_shape)) {
errors.push("OXC code shape must be auto, module, or snippet.");
}
} else if (
config.code_lang !== "python" &&
!VALIDATOR_SQL_CODE_LANGS.includes(config.code_lang)
) {
errors.push("Code validator code language must be python or sql dialect.");
}
}
if (config.kind === "seed") {
const seedSourceType = config.seed_source_type ?? "hf";
if (seedSourceType === "hf" && !config.hf_repo_id.trim()) {
errors.push("Seed dataset repo is required.");
}
if (!config.hf_path.trim()) {
errors.push("Seed metadata not loaded. Click 'Load columns + 10 rows'.");
}
if (
seedSourceType === "hf" &&
config.hf_endpoint?.trim() &&
!config.hf_endpoint.trim().startsWith("http")
) {
errors.push("HF endpoint must start with http.");
}
if (seedSourceType === "unstructured") {
if (config.drop && (config.seed_columns?.length ?? 0) === 0) {
errors.push("Seed drop needs loaded columns.");
}
const chunkSizeRaw = Number(config.unstructured_chunk_size);
const chunkOverlapRaw = Number(config.unstructured_chunk_overlap);
if (!Number.isFinite(chunkSizeRaw) || Math.floor(chunkSizeRaw) < 1) {
errors.push("Chunk size must be an integer >= 1.");
}
if (!Number.isFinite(chunkOverlapRaw) || Math.floor(chunkOverlapRaw) < 0) {
errors.push("Chunk overlap must be an integer >= 0.");
}
if (
Number.isFinite(chunkSizeRaw) &&
Number.isFinite(chunkOverlapRaw) &&
Math.floor(chunkOverlapRaw) >= Math.floor(chunkSizeRaw)
) {
errors.push("Chunk overlap must be less than chunk size.");
}
} else {
const selectedDropColumns = (config.seed_drop_columns ?? [])
.map((value) => value.trim())
.filter(Boolean);
if (selectedDropColumns.length > 0 && (config.seed_columns?.length ?? 0) === 0) {
errors.push("Seed drop columns need loaded columns.");
}
}
if (config.selection_type === "index_range") {
const start = parseIntNumber(config.selection_start);
const end = parseIntNumber(config.selection_end);
if (start === null || end === null) {
errors.push("Index range start/end must be integers.");
} else {
if (start < 0 || end < 0) {
errors.push("Index range start/end must be >= 0.");
}
if (end < start) {
errors.push("Index range end must be >= start.");
}
}
}
if (config.selection_type === "partition_block") {
const index = parseIntNumber(config.selection_index);
const parts = parseIntNumber(config.selection_num_partitions);
if (index === null || parts === null) {
errors.push("Partition index/num_partitions must be integers.");
} else {
if (index < 0) errors.push("Partition index must be >= 0.");
if (parts < 1) errors.push("Partition num_partitions must be >= 1.");
if (parts >= 1 && index >= parts) {
errors.push("Partition index must be < num_partitions.");
}
}
}
}
return errors;
}