import { claimedClassificationCheck, leakageWarnings } from "./dataset-profiler.mjs"; function cloneDecision(draft = {}) { return { task_type: draft.task_type || "classification", objective: draft.objective || "cross_entropy", primary_metric: draft.primary_metric || "ROC-AUC", split_strategy: draft.split_strategy || "random", group_split_column: draft.group_split_column || null, features: Array.isArray(draft.features) ? [...draft.features] : [], target: draft.target || null, confidence: draft.confidence || "high", requires_input_validation: Boolean(draft.requires_input_validation), input_constraints: Array.isArray(draft.input_constraints) ? [...draft.input_constraints] : [], input_validation_asserted: Boolean(draft.input_validation_asserted || draft.validation_asserted), gate_resolution: draft.gate_resolution || null }; } function percentText(value) { if (value == null || !Number.isFinite(value)) return null; return `${Number((value * 100).toFixed(4))}%`; } function pushQuestion(questions, text) { if (!questions.includes(text)) questions.push(text); } function result({ id, severity = "warn", fired = false, message, computed = {}, remedy = "", questions = [], resolution_status = fired ? "open" : "not_applicable", resolution_note = "" }) { return { id, severity, fired, message, computed, remedy, questions, resolution_status, resolution_note }; } function cleanNumber(value) { if (value === "" || value === null || value === undefined) return null; const number = Number(value); return Number.isFinite(number) ? number : null; } function cleanNonNegativeNumber(value, invalidAnswers, key) { const number = cleanNumber(value); if (number == null) return null; if (number < 0) { invalidAnswers[key] = "must be a non-negative number"; return null; } return number; } function cleanRecall(value, invalidAnswers) { const number = cleanNumber(value); if (number == null) return null; if (number <= 0 || number > 1) { invalidAnswers.minimum_recall = "must be greater than 0 and less than or equal to 1"; return null; } return number; } function cleanDate(value, invalidAnswers) { if (typeof value !== "string" || !value.trim()) return ""; const text = value.trim(); const parsed = Date.parse(text); if (!/^\d{4}-\d{2}-\d{2}(?:$|[tT\s])/.test(text) || !Number.isFinite(parsed)) { invalidAnswers.cutoff_date = "must be a valid ISO-like date such as 2026-03-01"; return ""; } return text; } function cleanGateAnswers(gateAnswers = {}) { const answers = gateAnswers && typeof gateAnswers === "object" ? gateAnswers : {}; const invalidAnswers = {}; return { false_negative_cost: cleanNonNegativeNumber(answers.false_negative_cost, invalidAnswers, "false_negative_cost"), false_positive_cost: cleanNonNegativeNumber(answers.false_positive_cost, invalidAnswers, "false_positive_cost"), minimum_recall: cleanRecall(answers.minimum_recall, invalidAnswers), cutoff_date: cleanDate(answers.cutoff_date, invalidAnswers), prediction_horizon: typeof answers.prediction_horizon === "string" ? answers.prediction_horizon.trim() : "", input_validation_acknowledged: Boolean(answers.input_validation_acknowledged), accepted_gate_ids: Array.isArray(answers.accepted_gate_ids) ? answers.accepted_gate_ids.map(String) : [], group_split_column: typeof answers.group_split_column === "string" ? answers.group_split_column.trim() : "", invalid_answers: invalidAnswers, leakage_field_known_before_prediction: answers.leakage_field_known_before_prediction && typeof answers.leakage_field_known_before_prediction === "object" ? answers.leakage_field_known_before_prediction : {} }; } function hasAcceptedGate(answers, id) { return answers.accepted_gate_ids.includes(id); } function gateResolution({ status, note = "", answers = {} }) { return { resolution_status: status, resolution_note: note, resolution_answers: answers }; } function normalizeLeakageAnswer(value) { if (value === false || value === "false" || value === "not_known_before_prediction") return false; if (value === true || value === "true" || value === "known_before_prediction") return true; return null; } function dateRangeForSignal(profile, signal) { const lower = String(signal || "").toLowerCase(); const column = (profile?.columns || []).find((item) => String(item.name).toLowerCase() === lower); if (!column?.date_min || !column?.date_max) return null; const min = Date.parse(column.date_min); const max = Date.parse(column.date_max); if (!Number.isFinite(min) || !Number.isFinite(max)) return null; return { column: column.name, min, max, minText: column.date_min, maxText: column.date_max }; } function cutoffDateIssue({ profile, check, cutoffDate }) { if (!cutoffDate) return ""; const cutoff = Date.parse(cutoffDate); if (!Number.isFinite(cutoff)) return ""; const ranges = (check.computed?.date_signals || []) .map((signal) => dateRangeForSignal(profile, signal)) .filter(Boolean); if (!ranges.length) return ""; const outside = ranges.find((range) => cutoff <= range.min || cutoff >= range.max); if (!outside) return ""; return `Cutoff date ${cutoffDate} must fall strictly inside ${outside.column}'s observed range (${outside.minText} to ${outside.maxText}) so train and test are both non-empty.`; } function resolveGate(check, answers, decision, { profile = null } = {}) { if (!check?.fired) return check; if (hasAcceptedGate(answers, check.id)) { return { ...check, ...gateResolution({ status: "accepted", note: "A user explicitly accepted this gate as a known implementation risk." }) }; } if (check.id === "metric-validity") { const hasCosts = answers.false_negative_cost > 0 && answers.false_positive_cost > 0; const hasRecall = answers.minimum_recall != null; if (hasCosts && hasRecall) { decision.threshold_policy = { false_negative_cost: answers.false_negative_cost, false_positive_cost: answers.false_positive_cost, minimum_recall: answers.minimum_recall }; return { ...check, ...gateResolution({ status: "resolved", note: "Business costs and minimum recall were supplied, so the metric gate can proceed with threshold tuning.", answers: decision.threshold_policy }) }; } return { ...check, resolution_status: "blocking", resolution_note: Object.keys(answers.invalid_answers).length ? "Supply non-negative false negative/false positive costs and a minimum recall in (0, 1] to resolve this gate." : "Supply false negative cost, false positive cost, and minimum recall to resolve this gate." }; } if (check.id === "split-validity") { const cutoffIssue = cutoffDateIssue({ profile, check, cutoffDate: answers.cutoff_date }); if (cutoffIssue) { return { ...check, resolution_status: "blocking", resolution_note: cutoffIssue }; } const needsGroupColumn = (check.computed?.group_signals || []).length > 0; if (needsGroupColumn && answers.group_split_column) { decision.group_split_column = answers.group_split_column; } if (answers.cutoff_date || answers.prediction_horizon) { decision.split_resolution = { cutoff_date: answers.cutoff_date || null, prediction_horizon: answers.prediction_horizon || null, group_split_column: decision.group_split_column || null }; return { ...check, ...gateResolution({ status: "resolved", note: "A temporal cutoff or prediction horizon was supplied for validation.", answers: decision.split_resolution }) }; } if (needsGroupColumn && answers.group_split_column) { decision.split_resolution = { cutoff_date: null, prediction_horizon: null, group_split_column: answers.group_split_column }; return { ...check, ...gateResolution({ status: "resolved", note: "A group split column was supplied for group-aware validation.", answers: decision.split_resolution }) }; } return { ...check, resolution_status: "blocking", resolution_note: needsGroupColumn ? "Supply the entity/group column to use for group-aware validation, and a cutoff or horizon too if time signals are present." : answers.invalid_answers.cutoff_date ? "Supply a valid cutoff date or prediction horizon to resolve this gate." : "Supply a cutoff date or prediction horizon to resolve this gate." }; } if (check.id === "target-leakage") { const columns = check.computed?.blocked_columns || []; const answered = columns.map((column) => [ column, normalizeLeakageAnswer(answers.leakage_field_known_before_prediction[column]) ]); const allAnswered = answered.length > 0 && answered.every(([, value]) => value !== null); const allExcluded = allAnswered && answered.every(([, value]) => value === false); if (allExcluded) { decision.feature_availability = { excluded_after_user_confirmation: columns }; return { ...check, ...gateResolution({ status: "resolved", note: "The user confirmed blocked leakage fields are not known before prediction, so exclusion resolves the gate.", answers: Object.fromEntries(answered) }) }; } if (allAnswered) { return { ...check, resolution_status: "blocking", resolution_note: `At least one blocked field was marked known before prediction; the feature contract must be reviewed before implementation. User assertions: ${Object.entries(Object.fromEntries(answered)) .map(([column, value]) => `${column}=${value}`) .join(", ")}.`, resolution_answers: Object.fromEntries(answered) }; } return { ...check, resolution_status: "blocking", resolution_note: "Confirm whether each blocked leakage field is known before prediction." }; } if (check.id === "data-contract-gate") { if (answers.input_validation_acknowledged) { decision.input_validation_asserted = true; return { ...check, ...gateResolution({ status: "resolved", note: "The user acknowledged generated input validation as a required implementation artifact.", answers: { input_validation_acknowledged: true } }) }; } return { ...check, resolution_status: check.severity === "block" ? "blocking" : "open", resolution_note: "Acknowledge generated runtime validation or accept the risk." }; } if (check.id === "train-test-overlap-gate") { return { ...check, resolution_status: "blocking", resolution_note: "Rebuild the holdout set so no rows or non-target feature fingerprints overlap with training data, or explicitly accept the risk." }; } return check; } function hasMlTaskSignal(claims, decision, profile) { if (profile?.inferred?.target) return true; if (claims.task_guess || claims.target_phrase || claims.resolved_target) return true; if ((claims.named_columns || []).some((column) => /(target|label|class|is_|_flag$|_label$)/i.test(column))) return true; return /\b(predict|prediction|detect|detection|classify|classification|forecast|forecasting|recommend|recommendation|rank|ranking|segment|cluster|estimate|estimation|score|model|train|optimization|dashboard|route|assign|assignment)\b/i.test(claims.raw || ""); } function learnabilityGate({ claims, profile, decision }) { const raw = String(claims.raw || ""); const lower = raw.toLowerCase(); const tokenCount = (lower.match(/[a-z0-9_]+/g) || []).length; const irreducibleRandomness = /\b(lottery|dice|roulette|coin toss|winning numbers?)\b/.test(lower); const nonMlBuild = /\b(website|login page|landing page|crud app|frontend|web page)\b/.test(lower) && !/\b(predict|classify|forecast|recommend|detect)\b/.test(lower); const noConcreteTarget = !profile?.inferred?.target && !claims.resolved_target && !claims.target_phrase && (!claims.named_columns || claims.named_columns.length === 0); const noTaskSignal = !hasMlTaskSignal(claims, decision, profile); const gibberish = tokenCount > 0 && tokenCount <= 5 && noConcreteTarget && noTaskSignal; const vague = /\b(make|build)\s+(?:an?\s+)?ai\b/.test(lower) && noConcreteTarget; const fired = irreducibleRandomness || nonMlBuild || noTaskSignal || gibberish || vague; if (!fired) { return result({ id: "learnability-gate", fired: false, message: "The request contains enough task and target signal to draft a learnable ML blueprint.", computed: { target: profile?.inferred?.target || claims.resolved_target || claims.target_phrase || decision.target || null, task_signal: true } }); } decision.confidence = "needs_resolution"; return result({ id: "learnability-gate", severity: "block", fired: true, message: "The request does not yet define a learnable ML objective, data target, or feasible prediction problem.", computed: { target: profile?.inferred?.target || claims.resolved_target || claims.target_phrase || null, no_task_signal: noTaskSignal, no_concrete_target: noConcreteTarget, irreducible_randomness: irreducibleRandomness, non_ml_build: nonMlBuild, gibberish, vague }, remedy: "Define the prediction target, available training examples, and the decision the model will support.", questions: ["What exact target should the model predict?", "What historical labeled data is available?", "What decision will use the prediction?"] }); } function isIdentifierLikeTarget(value) { return /^[A-Za-z_][A-Za-z0-9_]{0,59}$/.test(String(value || "").trim()); } function isNamedTarget(claims, target) { const lower = String(target || "").toLowerCase(); return Boolean(lower && (claims.named_columns || []).some((column) => String(column).toLowerCase() === lower)); } function identifiableTargetGate({ claims, profile, decision, projectType = "single_task" }) { if ( projectType === "multi_component_system" || profile?.inferred?.target || !["classification", "regression", "forecasting"].includes(decision.task_type) ) { return result({ id: "identifiable-target-gate", fired: false, message: "The target column is identified from dataset metadata, handled by component contracts, or the task does not require a supervised target.", computed: { target: profile?.inferred?.target || decision.target || null, project_type: projectType } }); } const target = String(decision.target || "").trim(); const namedTarget = isNamedTarget(claims, target); const phraseTarget = claims.target_phrase && target.toLowerCase() === String(claims.target_phrase).trim().toLowerCase() && !namedTarget; const placeholderTarget = /^(target|label|outcome)$/i.test(target) && !namedTarget; const invalidShape = !isIdentifierLikeTarget(target); const fired = !namedTarget && (phraseTarget || placeholderTarget || invalidShape); if (!fired) { return result({ id: "identifiable-target-gate", fired: false, message: "The target column name is identifier-shaped and was not derived from a raw target phrase.", computed: { target, named_target: namedTarget } }); } decision.confidence = "needs_resolution"; decision.target_identifier = { status: "unresolved", target, target_phrase: claims.target_phrase || null, named_columns: claims.named_columns || [] }; return result({ id: "identifiable-target-gate", severity: "block", fired: true, message: "The exact target column name could not be determined from the idea alone.", computed: { target, target_phrase: claims.target_phrase || null, named_columns: claims.named_columns || [], invalid_shape: invalidShape, phrase_target: Boolean(phraseTarget), placeholder_target: Boolean(placeholderTarget) }, remedy: "Name the exact target column, or attach/profile the dataset columns before generating runnable training code.", questions: ["What exact dataset column contains the target label?", "Can you attach or describe the dataset schema?"] }); } function csvClassificationCheck(profile) { return (profile?.executable_checks || []).find((check) => check.kind === "classification_majority_baseline") || null; } function classificationCheck({ claims, profile, decision }) { const csvCheck = csvClassificationCheck(profile); if (csvCheck) return csvCheck; if (decision.task_type !== "classification" && claims.task_guess !== "classification") return null; return claimedClassificationCheck(claims); } function dateSignals({ claims, profile }) { const signals = []; for (const column of profile?.inferred?.date_columns || []) signals.push(column); for (const column of claims.named_columns || []) { if (/(date|time|timestamp|signup|created|_at)\b/.test(column)) signals.push(column); } if (claims.has_time_language && signals.length === 0) { const match = claims.raw.toLowerCase().match( /\b(next (?:quarter|month|week|\d+ (?:days|weeks|months))|over time|forecast|real[- ]?time|stream(?:s|ing)?|early warning|monitor(?:ing)?|sensor|vital signs?)\b/ ); signals.push(match?.[1] || "time language"); } return Array.from(new Set(signals)); } function groupSignals({ profile }) { const inferred = profile?.inferred?.group_columns || []; const warningColumns = (profile?.split_warnings || []) .filter((warning) => /GroupKFold|GroupShuffleSplit|group-aware/i.test(warning.reason || "")) .map((warning) => warning.column); return Array.from(new Set([...inferred, ...warningColumns].filter(Boolean))); } export function buildSplitValidityCheck({ signals = [], groupSignals: entitySignals = [], splitStrategy = "random", id = "split-validity", context = "the idea/data" } = {}) { const dateSignals = Array.from(new Set((signals || []).filter(Boolean))); const entityGroupSignals = Array.from(new Set((entitySignals || []).filter(Boolean))); const temporalConflict = dateSignals.length > 0 && splitStrategy === "random"; const groupConflict = entityGroupSignals.length > 0 && !["group", "temporal_group"].includes(splitStrategy); const fired = temporalConflict || groupConflict; if (!fired) { return result({ id, fired: false, message: `No random split conflict with time or repeated-entity structure was detected for ${context}.`, computed: { date_signals: dateSignals, group_signals: entityGroupSignals, split_strategy: splitStrategy, shared_check: "split-validity" } }); } if (temporalConflict && !groupConflict) { return result({ id, severity: "block", fired: true, message: `Random train/test split is invalid for ${context} because it contains time signal(s): ${dateSignals.join(", ")}.`, computed: { date_signals: dateSignals, group_signals: entityGroupSignals, previous_split: splitStrategy, shared_check: "split-validity" }, remedy: "Use temporal validation such as a cutoff date, rolling split, or TimeSeriesSplit.", questions: ["Cutoff date separating train/test?", "Prediction horizon?"] }); } if (groupConflict && !temporalConflict) { return result({ id, severity: "block", fired: true, message: `${splitStrategy === "random" ? "Random" : "Current"} train/test split is invalid for ${context} because rows repeat entity/group value(s): ${entityGroupSignals.join(", ")}.`, computed: { date_signals: dateSignals, group_signals: entityGroupSignals, previous_split: splitStrategy, shared_check: "split-validity" }, remedy: "Use group-aware validation such as GroupKFold or GroupShuffleSplit so the same entity never appears in both train and test.", questions: [`Group column for validation (${entityGroupSignals[0] || "entity_id"})?`] }); } return result({ id, severity: "block", fired: true, message: `Random train/test split is invalid for ${context} because it contains time signal(s): ${dateSignals.join(", ")} and repeated entity/group value(s): ${entityGroupSignals.join(", ")}.`, computed: { date_signals: dateSignals, group_signals: entityGroupSignals, previous_split: splitStrategy, shared_check: "split-validity" }, remedy: "Use temporal validation with group-aware boundaries, such as an out-of-time holdout that keeps each entity in only one split.", questions: ["Cutoff date separating train/test?", "Prediction horizon?", `Group column for validation (${entityGroupSignals[0] || "entity_id"})?`] }); } function isActionableLeakageWarn(warning) { const reason = String(warning?.reason || ""); return /aggregate-style name|alone predicts|alone reproduces|Column name suggests/i.test(reason); } function profileLeakageCandidates(profile) { return (profile?.leakage_warnings || []).filter( (warning) => warning.severity === "block" || (warning.severity === "warn" && isActionableLeakageWarn(warning)) ); } function claimLeakageBlocks(claims) { if (!claims.named_columns?.length) return []; const pseudoColumns = claims.named_columns.map((name) => ({ name })); return leakageWarnings(pseudoColumns, null, { targetPhrase: claims.target_phrase || claims.raw }).filter( (warning) => warning.severity === "block" ); } function metricValidity({ claims, profile, decision }) { const check = classificationCheck({ claims, profile, decision }); const accuracyClaim = claims.stated_objective === "accuracy" || decision.objective === "accuracy"; const rareClaim = claims.positive_rate != null && claims.positive_rate <= 0.05; const severeImbalance = check?.majority_accuracy != null && check.majority_accuracy >= 0.8; const fired = Boolean(check && (severeImbalance || (accuracyClaim && rareClaim))); if (!fired) { return result({ id: "metric-validity", fired: false, message: "No severe class-imbalance metric conflict was detected.", computed: check || {} }); } decision.objective = "cross_entropy"; decision.primary_metric = "average_precision"; decision.confidence = "needs_resolution"; const claimedNumber = check.kind === "classification_majority_baseline_claimed" && claims.positive_rate != null ? ` The idea states ${percentText(claims.positive_rate)} positive rate` : ""; const objectiveText = claims.stated_objective_raw ? ` and asks for ${claims.stated_objective_raw}.` : "."; return result({ id: "metric-validity", severity: "block", fired: true, message: `${check.executable_consequence}${claimedNumber}${claims.stated_objective_raw ? objectiveText : ""}`, computed: check, remedy: "Use probability loss with PR-AUC/average precision, recall targets, and threshold tuning instead of accepting accuracy.", questions: ["Cost of a missed positive vs a false alarm?", "Minimum acceptable recall?"] }); } function splitValidity({ claims, profile, decision }) { const signals = dateSignals({ claims, profile }); const entitySignals = groupSignals({ profile }); const check = buildSplitValidityCheck({ signals, groupSignals: entitySignals, splitStrategy: decision.split_strategy, context: "the idea/data" }); if (!check.fired) return check; if (check.computed?.date_signals?.length && check.computed?.group_signals?.length) { decision.split_strategy = "temporal_group"; decision.group_split_column = check.computed.group_signals[0]; } else if (check.computed?.group_signals?.length) { decision.split_strategy = "group"; decision.group_split_column = check.computed.group_signals[0]; } else { decision.split_strategy = "temporal"; } decision.confidence = "needs_resolution"; return check; } function targetLeakage({ claims, profile, decision }) { const blocks = profile ? profileLeakageCandidates(profile) : claimLeakageBlocks(claims); if (!blocks.length) { return result({ id: "target-leakage", fired: false, message: "No blocking target leakage columns were detected.", computed: { blocked_columns: [] } }); } const blockedColumns = Array.from(new Set(blocks.map((warning) => warning.column))); const blockedSet = new Set(blockedColumns.map((column) => column.toLowerCase())); decision.features = decision.features.filter((feature) => !blockedSet.has(String(feature).toLowerCase())); decision.confidence = "needs_resolution"; const target = decision.target || claims.target_phrase || "target"; const severity = blocks.some((warning) => warning.severity === "block") ? "block" : "warn"; return result({ id: "target-leakage", severity, fired: true, message: `Remove leakage column(s) ${blockedColumns.join(", ")} before predicting ${target}.`, computed: { blocked_columns: blockedColumns, warnings: blocks }, remedy: "Drop blocked leakage columns from features and schema, then ask whether any remaining target-like fields are known before prediction time.", questions: blockedColumns.map((column) => `Is ${column} known strictly before the prediction date?`) }); } function trainTestOverlapGate({ profile }) { const overlap = profile?.holdout_overlap || null; const exactDuplicates = overlap?.exact_duplicate_rows || 0; const featureDuplicates = overlap?.feature_duplicate_rows || 0; const extraFeatureDuplicates = Math.max(0, featureDuplicates - exactDuplicates); const fired = exactDuplicates > 0 || extraFeatureDuplicates > 0; if (!fired) { return result({ id: "train-test-overlap-gate", fired: false, message: "No duplicate train/holdout row overlap was detected.", computed: { holdout_overlap: overlap, advisory_policy: "warn-severity quality_warnings stay advisory-only by design; block-severity holdout contamination routes through this gate." } }); } const duplicateText = [ exactDuplicates > 0 ? `${exactDuplicates} exact duplicate holdout row(s)` : "", extraFeatureDuplicates > 0 ? `${extraFeatureDuplicates} additional feature-duplicate holdout row(s)` : "" ].filter(Boolean); return result({ id: "train-test-overlap-gate", severity: "block", fired: true, message: `Holdout contamination detected: ${duplicateText.join(" and ")} overlap with training data.`, computed: { holdout_overlap: overlap, exact_duplicate_rows: exactDuplicates, feature_duplicate_rows: featureDuplicates, advisory_policy: "warn-severity quality_warnings stay advisory-only by design; block-severity holdout contamination routes through this gate." }, remedy: "Rebuild the holdout set so no rows or non-target feature fingerprints overlap with training data, then re-profile the train/holdout pair.", questions: ["Can you provide a de-duplicated holdout file?"] }); } function profileColumn(profile, field) { return (profile?.columns || []).find((column) => String(column.name).toLowerCase() === String(field).toLowerCase()) || null; } function isIdLike(field, profile) { const lower = String(field || "").toLowerCase(); return /(^id$|_id$|^uuid$|guid)/.test(lower) || (profile?.inferred?.id_columns || []).some((column) => column.toLowerCase() === lower); } function isTimestampLike(field, profile) { const lower = String(field || "").toLowerCase(); return ( /(^timestamp$|_timestamp$|_date$|_at$|date|created|updated|time)/.test(lower) || (profile?.inferred?.date_columns || []).some((column) => column.toLowerCase() === lower) ); } function inferFieldConstraint(field, profile = null) { const lower = String(field || "").toLowerCase(); const column = profileColumn(profile, field); const nullable = Boolean(column && column.missing_count > 0); if (column) { if (column.kind === "id") { return { field, kind: "id", rule: "non-empty string", nullable }; } if (column.kind === "date") { return { field, kind: "timestamp", rule: "parseable datetime", nullable }; } if (column.kind === "numeric") { const min = column.numeric_min; const max = column.numeric_max; const nonNegative = column.numeric_nonnegative_ratio == null || column.numeric_nonnegative_ratio >= 0.98; const integerLike = column.numeric_integer_ratio != null && column.numeric_integer_ratio >= 0.98; if (Number.isFinite(min) && Number.isFinite(max) && min >= 0 && max <= 1) { return { field, kind: "probability", rule: "0 <= x <= 1", nullable }; } if (integerLike && nonNegative && column.unique_count <= 20 && column.unique_ratio <= 0.2) { return { field, kind: "count", rule: "integer x >= 0", nullable }; } return { field, kind: "number", rule: "finite number", nullable }; } if (column.kind === "categorical" || column.kind === "boolean") { return { field, kind: "categorical", rule: "non-empty string or known category", nullable }; } if (column.kind === "text") { return { field, kind: "text", rule: "non-empty string", nullable }; } } if (isIdLike(field, profile)) { return { field, kind: "id", rule: "non-empty string", nullable }; } if (isTimestampLike(field, profile)) { return { field, kind: "timestamp", rule: "parseable datetime", nullable }; } if (/(_prob|_probability|probability|_risk|risk|_rate|rate)$/.test(lower) || /(^risk_|_risk_)/.test(lower)) { return { field, kind: "probability", rule: "0 <= x <= 1", nullable }; } if (/(^amount$|_amount$|price|balance|value|cost)/.test(lower)) { return { field, kind: "amount", rule: "x >= 0", nullable }; } if (/(_count$|^num_|_chargebacks$|chargeback_count|age_days$|_days$)/.test(lower)) { return { field, kind: "count", rule: "integer x >= 0", nullable }; } if (column?.kind === "numeric" && column.sample_values?.length) { const values = column.sample_values.map(Number).filter((value) => Number.isFinite(value)); if (values.length && values.every((value) => value >= 0 && value <= 1)) { return { field, kind: "probability", rule: "0 <= x <= 1", nullable }; } } if (column?.kind === "categorical" || column?.kind === "boolean" || (column && column.unique_ratio <= 0.2)) { return { field, kind: "categorical", rule: "non-empty string or known category", nullable }; } return { field, kind: "unknown", rule: "type must be asserted", nullable }; } function dataContractGate({ profile, decision }) { if (decision.input_validation_asserted || (decision.requires_input_validation && decision.input_constraints.length)) { return result({ id: "data-contract-gate", fired: false, message: "Input validation layer is already asserted.", computed: { fields: decision.input_constraints || [], unvalidated_count: 0 } }); } const targetLower = decision.target ? String(decision.target).toLowerCase() : null; const features = Array.from(new Set((decision.features || []).filter(Boolean))).filter( (feature) => String(feature).toLowerCase() !== targetLower && !isIdLike(feature, profile) ); const fields = features .map((feature) => inferFieldConstraint(feature, profile)) .filter((field) => field.kind !== "unknown" || /(score|signal|metric|index|ratio)/i.test(field.field)); const actionable = fields.filter((field) => field.kind !== "id"); const hardFields = actionable.filter((field) => ["probability", "amount", "timestamp"].includes(field.kind)); const unknownFields = actionable.filter((field) => field.kind === "unknown"); if (!actionable.length) { return result({ id: "data-contract-gate", fired: false, message: "No runtime feature inputs requiring boundary validation were detected.", computed: { fields: [], unvalidated_count: 0 } }); } decision.input_constraints = actionable; decision.requires_input_validation = true; if (unknownFields.length) decision.confidence = "needs_resolution"; const summaries = actionable.map((field) => `${field.field}: ${field.rule}`); const questions = unknownFields.map((field) => `Is ${field.field} a probability [0,1], non-negative amount/count, timestamp, category, or another type?`); const firedBlock = hardFields.length > 0; return result({ id: "data-contract-gate", severity: firedBlock ? "block" : "warn", fired: true, message: `Inputs ${summaries.join("; ")} are consumed without boundary validation. Reject booleans passed as numbers for probability, amount, and count fields.`, computed: { fields: actionable, unvalidated_count: actionable.length }, remedy: { require_validation: true, constraints: actionable }, questions }); } export function evaluateBlueprint({ claims = {}, profile = null, draft = {}, gateAnswers = {}, projectType = "single_task" } = {}) { const decision = cloneDecision(draft); const answers = cleanGateAnswers(gateAnswers); const checks = [ learnabilityGate({ claims, profile, decision }), metricValidity({ claims, profile, decision }), splitValidity({ claims, profile, decision }), targetLeakage({ claims, profile, decision }), trainTestOverlapGate({ profile }), identifiableTargetGate({ claims, profile, decision, projectType }), dataContractGate({ claims, profile, decision }) ].map((check) => resolveGate(check, answers, decision, { profile })); const generatedQuestions = []; const blocking = checks.filter( (check) => check.fired && check.severity === "block" && !["resolved", "accepted"].includes(check.resolution_status) ); const accepted = checks.filter((check) => check.fired && check.resolution_status === "accepted"); const resolved = checks.filter((check) => check.fired && check.resolution_status === "resolved"); const openWarnings = checks.filter( (check) => check.fired && check.severity === "warn" && !["resolved", "accepted"].includes(check.resolution_status) ); const unresolvedWarningNeedsResolution = openWarnings.length > 0 && decision.confidence === "needs_resolution"; if (!blocking.length && !unresolvedWarningNeedsResolution && decision.confidence === "needs_resolution") { decision.confidence = accepted.length ? "medium" : "high"; } else if (accepted.length && decision.confidence === "high") { decision.confidence = "medium"; } for (const check of checks.filter( (item) => item.fired && !["resolved", "accepted"].includes(item.resolution_status) && (item.severity === "block" || item.questions?.length) )) { for (const question of check.questions || []) pushQuestion(generatedQuestions, question); } const knownGateIds = new Set(checks.map((check) => check.id)); decision.gate_resolution = { answers, invalid_answers: answers.invalid_answers, resolved_gate_ids: resolved.map((check) => check.id), accepted_gate_ids: accepted.map((check) => check.id), ignored_accepted_gate_ids: answers.accepted_gate_ids.filter((id) => !knownGateIds.has(id)), unresolved_gate_ids: blocking.map((check) => check.id) }; return { verdict: blocking.length ? "needs_resolution" : accepted.length || unresolvedWarningNeedsResolution ? "warn" : "ok", blocking, resolved, accepted, all: checks, generated_questions: generatedQuestions, decision }; }