import assert from "node:assert/strict"; import test from "node:test"; import { generateBlueprint } from "../blueprint-engine.mjs"; import { analyzeDataset } from "../dataset-profiler.mjs"; import { parseIdeaClaims } from "../idea-claims.mjs"; import { callTool } from "../mcp-server.mjs"; function imbalancedCsv() { const rows = ["customer_id,usage_minutes,plan,churn"]; for (let index = 1; index <= 94; index += 1) rows.push(`c${index},${100 + index},pro,0`); for (let index = 95; index <= 100; index += 1) rows.push(`c${index},${100 + index},basic,1`); return rows.join("\n"); } const fraudNoCsvDemoIdea = "Detect fraudulent transactions from our payments table with timestamp, amount, merchant_id, and a is_fraud label. 0.7% of transactions are fraud. I want the model to be accurate."; const overlapIdea = "Predict whether a loan applicant will default, using applicant demographics and credit history."; const overlapTrainCsv = [ "applicant_id,age,zip_code,email,credit_score,constant_flag,default_within_12m", "A001,34,10001,a@example.com,650,yes,0", "A001,34,10001,a@example.com,650,yes,1", "A002,45,10002,b@example.com,720,yes,0", "A002,45,10002,b@example.com,720,yes,0", "A003,29,10003,c@example.com,580,yes,1", "A004,52,10004,d@example.com,760,yes,0", "A005,38,10005,e@example.com,610,yes,1", "A005,38,10005,e@example.com,610,yes,1", "A006,41,10006,f@example.com,690,yes,0", "A007,25,10007,g@example.com,560,yes,1" ].join("\n"); const contaminatedHoldoutCsv = [ "applicant_id,age,zip_code,email,credit_score,constant_flag,default_within_12m", "A001,34,10001,a@example.com,650,yes,1", "A010,30,10010,h@example.com,600,yes,0" ].join("\n"); const cleanHoldoutCsv = [ "applicant_id,age,zip_code,email,credit_score,constant_flag,default_within_12m", "A010,30,10010,h@example.com,600,yes,0", "A011,36,10011,i@example.com,640,yes,1" ].join("\n"); function overlapGate(blueprint) { return blueprint.consequences.all.find((gate) => gate.id === "train-test-overlap-gate"); } function overlapGateAnswers() { return { group_split_column: "applicant_id", input_validation_acknowledged: true }; } test("fraud idea without CSV blocks accuracy from claimed positive rate", () => { const idea = "0.7% of transactions are fraud. I want the model to be accurate."; const claims = parseIdeaClaims(idea); const blueprint = generateBlueprint({ idea, task: "auto", audience: "technical" }); assert.equal(claims.positive_rate, 0.007); assert.equal(claims.stated_objective, "accuracy"); assert.equal(blueprint.consequences.verdict, "needs_resolution"); assert.equal(blueprint.decision.primary_metric, "average_precision"); assert.equal(blueprint.decision.confidence, "needs_resolution"); assert.equal(blueprint.confidence, "Needs resolution"); assert.notEqual(blueprint.decision.objective, "accuracy"); const metricBlock = blueprint.consequences.blocking.find((block) => block.id === "metric-validity"); assert.ok(metricBlock); assert.match(metricBlock.message, /0\.7%/); assert.match(metricBlock.message, /0\.993/); assert.match(metricBlock.message, /recall is 0/); assert.ok(blueprint.consequences.blocking.some((block) => block.id === "identifiable-target-gate")); }); test("business cost answers resolve the metric-validity gate", () => { const idea = "0.7% of transactions are fraud. I want the model to be accurate."; const blueprint = generateBlueprint({ idea, task: "classification", audience: "technical", gate_answers: { false_negative_cost: 500, false_positive_cost: 25, minimum_recall: 0.85 } }); assert.equal(blueprint.consequences.verdict, "needs_resolution"); assert.ok(!blueprint.consequences.blocking.some((item) => item.id === "metric-validity")); assert.ok(blueprint.consequences.blocking.some((item) => item.id === "identifiable-target-gate")); assert.ok(blueprint.consequences.resolved.some((item) => item.id === "metric-validity")); assert.equal(blueprint.decision.threshold_policy.false_negative_cost, 500); assert.equal(blueprint.decision.threshold_policy.false_positive_cost, 25); assert.equal(blueprint.decision.threshold_policy.minimum_recall, 0.85); assert.equal(blueprint.decision.confidence, "needs_resolution"); assert.ok(!blueprint.generated_questions.some((question) => /Cost of a missed positive/.test(question))); }); test("gate answers can resolve metric, split, and validation gates together", () => { const blueprint = generateBlueprint({ idea: fraudNoCsvDemoIdea, task: "auto", audience: "technical", gate_answers: { false_negative_cost: 500, false_positive_cost: 25, minimum_recall: 0.9, cutoff_date: "2026-03-01", input_validation_acknowledged: true } }); const resolvedIds = blueprint.consequences.resolved.map((item) => item.id); assert.deepEqual(blueprint.decision.gate_resolution.unresolved_gate_ids, []); assert.deepEqual(blueprint.consequences.blocking, []); assert.ok(resolvedIds.includes("metric-validity")); assert.ok(resolvedIds.includes("split-validity")); assert.ok(resolvedIds.includes("data-contract-gate")); assert.equal(blueprint.decision.split_resolution.cutoff_date, "2026-03-01"); assert.equal(blueprint.decision.input_validation_asserted, true); assert.deepEqual(blueprint.agent_spec.gate_resolution.unresolved_gate_ids, []); }); test("v2.1 fraud demo resolves is_fraud as target and timestamp as temporal split", () => { const claims = parseIdeaClaims(fraudNoCsvDemoIdea); const blueprint = generateBlueprint({ idea: fraudNoCsvDemoIdea, task: "auto", audience: "technical" }); const blockIds = blueprint.consequences.blocking.map((block) => block.id); const splitBlock = blueprint.consequences.blocking.find((block) => block.id === "split-validity"); const renderedText = [ blueprint.summary.Optimization, ...blueprint.data_contract, ...blueprint.model_path ].join("\n"); assert.ok(claims.named_columns.includes("timestamp")); assert.ok(claims.named_columns.includes("amount")); assert.ok(claims.named_columns.includes("merchant_id")); assert.ok(claims.named_columns.includes("is_fraud")); assert.equal(claims.resolved_target, "is_fraud"); assert.ok(!claims.resolved_features.includes("is_fraud")); assert.ok(!claims.resolved_features.includes("merchant_id")); assert.equal(claims.has_time_language, true); assert.equal(blueprint.decision.target, "is_fraud"); assert.ok(!blueprint.decision.features.includes("is_fraud")); assert.ok(!blueprint.decision.features.includes("merchant_id")); assert.ok(blueprint.decision.features.includes("timestamp")); assert.ok(blueprint.decision.features.includes("amount")); assert.ok(blockIds.includes("metric-validity")); assert.ok(blockIds.includes("split-validity")); assert.equal(splitBlock.fired, true); assert.match(splitBlock.message, /timestamp/); assert.equal(blueprint.decision.split_strategy, "temporal"); assert.equal(blueprint.decision.primary_metric, "average_precision"); assert.equal(blueprint.confidence, "Needs resolution"); assert.match(blueprint.files["train.py"], /TARGET = "is_fraud"/); assert.match(blueprint.files["train.py"], /"timestamp"/); assert.match(blueprint.files["train.py"], /"amount"/); assert.doesNotMatch(blueprint.files["train.py"].match(/FEATURES = \[[\s\S]*?\]/)?.[0] || "", /"is_fraud"/); assert.doesNotMatch(renderedText, /\brandom\b/i); assert.doesNotMatch(renderedText, /\bROC-AUC\b/i); assert.doesNotMatch(renderedText, /\baccuracy\b/i); }); test("software-build fraud scenario parses table has column prose", () => { const idea = "Build a fraud detection web service for our payments table. The table has timestamp, amount, merchant_id, user_id, card_country, device_type, and a is_fraud label. Only 0.7% of transactions are fraud. I want the model to be accurate and expose an API endpoint that scores new transactions."; const blueprint = generateBlueprint({ idea, task: "auto", audience: "technical" }); assert.deepEqual(blueprint.claims.named_columns, [ "timestamp", "amount", "merchant_id", "user_id", "card_country", "device_type", "is_fraud" ]); assert.equal(blueprint.decision.target, "is_fraud"); assert.deepEqual(blueprint.decision.features, ["timestamp", "amount", "card_country", "device_type"]); assert.equal(blueprint.decision.split_strategy, "temporal"); assert.equal(blueprint.decision.primary_metric, "average_precision"); assert.ok(blueprint.consequences.blocking.some((block) => block.id === "metric-validity")); assert.ok(blueprint.consequences.blocking.some((block) => block.id === "split-validity")); }); test("fraud CSV wins over claimed checks and blocks accuracy", () => { const idea = "predict churn from customer activity; I want the model to be accurate"; const profile = analyzeDataset({ csvText: imbalancedCsv(), filename: "imbalanced_churn.csv", idea }); const blueprint = generateBlueprint({ idea, task: "auto", audience: "technical", dataset_profile: profile }); const check = blueprint.dataset_profile.executable_checks[0]; assert.equal(check.majority_accuracy, 0.94); assert.equal(check.minority_recall, 0); assert.equal(blueprint.consequences.verdict, "needs_resolution"); assert.equal(blueprint.decision.primary_metric, "average_precision"); assert.match(blueprint.files["train.py"], /average_precision_score/); }); test("train-test overlap gate blocks contaminated holdout files", () => { const profile = analyzeDataset({ csvText: overlapTrainCsv, filename: "train.csv", holdoutCsvText: contaminatedHoldoutCsv, holdoutFilename: "holdout.csv", idea: overlapIdea }); const blueprint = generateBlueprint({ idea: overlapIdea, task: "classification", audience: "technical", dataset_profile: profile, gate_answers: overlapGateAnswers() }); const gate = overlapGate(blueprint); assert.equal(profile.holdout_overlap.exact_duplicate_rows, 1); assert.ok(profile.quality_warnings.some((warning) => warning.column === "train_test_overlap" && warning.severity === "block")); assert.equal(gate.fired, true); assert.equal(gate.severity, "block"); assert.equal(gate.resolution_status, "blocking"); assert.ok(blueprint.consequences.blocking.some((block) => block.id === "train-test-overlap-gate")); assert.match(gate.computed.advisory_policy, /warn-severity quality_warnings stay advisory-only/i); }); test("train-test overlap gate can still be explicitly accepted", () => { const profile = analyzeDataset({ csvText: overlapTrainCsv, filename: "train.csv", holdoutCsvText: contaminatedHoldoutCsv, holdoutFilename: "holdout.csv", idea: overlapIdea }); const blueprint = generateBlueprint({ idea: overlapIdea, task: "classification", audience: "technical", dataset_profile: profile, gate_answers: { ...overlapGateAnswers(), accepted_gate_ids: ["train-test-overlap-gate"] } }); const gate = overlapGate(blueprint); assert.equal(gate.fired, true); assert.equal(gate.resolution_status, "accepted"); assert.equal(blueprint.consequences.blocking.some((block) => block.id === "train-test-overlap-gate"), false); assert.ok(blueprint.consequences.accepted.some((item) => item.id === "train-test-overlap-gate")); }); test("train-test overlap gate is not applicable without holdout data", () => { const profile = analyzeDataset({ csvText: overlapTrainCsv, filename: "train.csv", idea: overlapIdea }); const blueprint = generateBlueprint({ idea: overlapIdea, task: "classification", audience: "technical", dataset_profile: profile, gate_answers: overlapGateAnswers() }); const gate = overlapGate(blueprint); assert.equal(profile.holdout_overlap, null); assert.equal(gate.fired, false); assert.equal(blueprint.consequences.blocking.some((block) => block.id === "train-test-overlap-gate"), false); }); test("train-test overlap gate does not fire for distinct holdout rows", () => { const profile = analyzeDataset({ csvText: overlapTrainCsv, filename: "train.csv", holdoutCsvText: cleanHoldoutCsv, holdoutFilename: "holdout.csv", idea: overlapIdea }); const blueprint = generateBlueprint({ idea: overlapIdea, task: "classification", audience: "technical", dataset_profile: profile, gate_answers: overlapGateAnswers() }); const gate = overlapGate(blueprint); assert.notEqual(profile.holdout_overlap, null); assert.equal(profile.holdout_overlap.exact_duplicate_rows, 0); assert.equal(profile.holdout_overlap.feature_duplicate_rows, 0); assert.equal(gate.fired, false); assert.equal(blueprint.consequences.blocking.some((block) => block.id === "train-test-overlap-gate"), false); }); test("train-test overlap gate fails score and export readiness", async () => { const profileResponse = await callTool("mille_profile_dataset", { csv_text: overlapTrainCsv, filename: "train.csv", holdout_csv_text: contaminatedHoldoutCsv, holdout_filename: "holdout.csv", idea: overlapIdea }); const profile = profileResponse.structuredContent.profile; const blueprintResponse = await callTool("mille_generate_blueprint", { idea: overlapIdea, task: "classification", audience: "technical", dataset_profile: profile, gate_answers: overlapGateAnswers() }); const blueprint = blueprintResponse.structuredContent.blueprint; const scoreResponse = await callTool("mille_score_blueprint", { blueprint }); const exportResponse = await callTool("mille_export_project", { idea: overlapIdea, task: "classification", audience: "technical", dataset_profile: profile, gate_answers: overlapGateAnswers(), include_zip_base64: true }); assert.ok(blueprint.consequences.blocking.some((block) => block.id === "train-test-overlap-gate")); assert.notEqual(scoreResponse.structuredContent.verdict, "ready"); assert.ok(scoreResponse.structuredContent.score < 100); assert.equal( scoreResponse.structuredContent.checks.find((check) => check.id === "no_blocking_gates").passed, false ); assert.equal(exportResponse.structuredContent.export_allowed, false); assert.equal("zip_base64" in exportResponse.structuredContent, false); assert.ok( exportResponse.structuredContent.blocking_gates.some((gate) => gate.id === "train-test-overlap-gate") ); }); test("revenue idea without CSV blocks random split and aggregate leakage", () => { const idea = "We have a customer table with signup_date, total_payments_to_date, last_payment_date, current_mrr, and lifetime_value. Predict next-quarter revenue. Use a normal train/test split."; const claims = parseIdeaClaims(idea); const blueprint = generateBlueprint({ idea, task: "auto", audience: "technical" }); const blockIds = blueprint.consequences.blocking.map((block) => block.id); const leakageBlock = blueprint.consequences.blocking.find((block) => block.id === "target-leakage"); assert.equal(claims.stated_split, "random"); assert.equal(claims.has_time_language, true); assert.ok(claims.named_columns.includes("lifetime_value")); assert.ok(claims.named_columns.includes("total_payments_to_date")); assert.ok(blockIds.includes("split-validity")); assert.ok(blockIds.includes("target-leakage")); assert.equal(blueprint.decision.split_strategy, "temporal"); assert.ok(leakageBlock.computed.blocked_columns.includes("lifetime_value")); assert.ok(leakageBlock.computed.blocked_columns.includes("total_payments_to_date")); assert.ok(!blueprint.decision.features.includes("lifetime_value")); assert.ok(!blueprint.decision.features.includes("total_payments_to_date")); assert.match(blueprint.files["train.py"], /TimeSeriesSplit/); assert.doesNotMatch(blueprint.files["train.py"], /train_test_split/); assert.doesNotMatch(blueprint.files["schema.yaml"], /lifetime_value/); assert.doesNotMatch(blueprint.files["schema.yaml"], /total_payments_to_date/); }); test("clean house price idea does not create false blocking consequences", () => { const idea = "Predict house price. Table has columns property_id, sqft, location, bedrooms, price. Use an out-of-time split."; const blueprint = generateBlueprint({ idea, task: "auto", audience: "technical" }); assert.equal(blueprint.consequences.verdict, "ok"); assert.equal(blueprint.consequences.blocking.length, 0); assert.notEqual(blueprint.confidence, "Needs resolution"); });