Millie / consequence-core.mjs
AhmedMSLTI's picture
Fix Millie multigate interactions
85f5009
Raw
History Blame Contribute Delete
36 kB
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
};
}