258 lines
8.6 KiB
TypeScript
258 lines
8.6 KiB
TypeScript
import type { NodeConfig } from "../types";
|
|
|
|
function parseNumber(value?: string): number | null {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? num : null;
|
|
}
|
|
|
|
function parseIntNumber(value?: string): number | null {
|
|
const num = parseNumber(value);
|
|
if (num === null || !Number.isInteger(num)) {
|
|
return null;
|
|
}
|
|
return num;
|
|
}
|
|
|
|
function parseAgeRange(value?: string): [number, number] | null {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
const parts = value.split(/[^0-9.]+/).filter(Boolean);
|
|
if (parts.length !== 2) {
|
|
return null;
|
|
}
|
|
const min = Number(parts[0]);
|
|
const max = Number(parts[1]);
|
|
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
return null;
|
|
}
|
|
return [min, max];
|
|
}
|
|
|
|
// 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") {
|
|
if (config.person_sex?.trim()) {
|
|
const normalized = config.person_sex.trim();
|
|
if (!(normalized === "Male" || normalized === "Female")) {
|
|
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.sampler_type === "person_from_faker") {
|
|
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.kind === "expression") {
|
|
if (!config.expr.trim()) {
|
|
errors.push("Expression is required.");
|
|
}
|
|
}
|
|
if (config.kind === "seed") {
|
|
if (!config.hf_path.trim()) {
|
|
errors.push("HF dataset path is required.");
|
|
}
|
|
if (config.hf_endpoint?.trim() && !config.hf_endpoint.trim().startsWith("http")) {
|
|
errors.push("HF endpoint must start with http.");
|
|
}
|
|
if (config.drop && (config.seed_columns?.length ?? 0) === 0) {
|
|
errors.push("Seed drop needs loaded columns (open Seed Preview).");
|
|
}
|
|
|
|
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;
|
|
}
|