diff --git a/benchmark-contamination-graph-guard/README.md b/benchmark-contamination-graph-guard/README.md new file mode 100644 index 00000000..993acd4f --- /dev/null +++ b/benchmark-contamination-graph-guard/README.md @@ -0,0 +1,47 @@ +# Benchmark Contamination Graph Guard + +This contribution adds a focused guardrail for SCIBASE's Scientific Knowledge Graph Integration. It evaluates model-to-benchmark graph edges before entity pages, benchmark badges, or AI recommendations treat an evaluation result as clean independent evidence. + +The slice is intentionally narrow. It does not rebuild graph ingestion, entity extraction, ontology aliasing, geospatial provenance, organism or strain boundaries, chemical identity, software dependency provenance, calibration evidence, negative evidence, or recommendation path explanation. It covers the model evaluation leakage question: did the model already see the benchmark, labels, split, or derived data? + +## What It Checks + +- Model graph edges such as `trained_on`, `pretrained_on`, `fine_tuned_on`, `derived_from`, and `label_source`. +- Benchmark claim edges such as `evaluated_on`, `claims_sota_on`, `recommended_for`, and `compared_against`. +- Direct training/pretraining overlap with the claimed benchmark. +- Training/evaluation split hash collisions. +- Shared training and benchmark label sources. +- Benchmark release date versus model training cutoff. +- Independent holdout evidence, split checksums, and locked evaluation-set evidence. +- Unsafe entity-page and recommendation publication when contamination risk is unresolved. + +## Running Locally + +```bash +npm test +npm run demo +npm run check +npm run video +``` + +The demo uses only synthetic graph packets in `data/sample_benchmark_graph_packets.json`. + +Generated files: + +- `reports/summary.json` +- `reports/reviewer-packet.md` +- `reports/summary.svg` +- `reports/demo.mp4` + +The MP4 is generated from an FFmpeg color source and drawtext slate. It is not a screen recording and contains no desktop capture or private information. + +## Why This Matters + +Knowledge graphs can amplify benchmark claims through entity pages, related-work recommendations, and model/dataset discovery surfaces. If a model was trained on a benchmark, reused its labels, or leaked evaluation examples through derived datasets, the graph should not present that benchmark edge as clean evidence. + +This guard gives curators a deterministic review packet so SCIBASE can: + +1. Suppress contaminated benchmark badges. +2. Downgrade recommendation confidence. +3. Preserve split and label-source evidence. +4. Require independent holdout proof before publishing model evaluation graph edges. diff --git a/benchmark-contamination-graph-guard/data/sample_benchmark_graph_packets.json b/benchmark-contamination-graph-guard/data/sample_benchmark_graph_packets.json new file mode 100644 index 00000000..2640ded3 --- /dev/null +++ b/benchmark-contamination-graph-guard/data/sample_benchmark_graph_packets.json @@ -0,0 +1,165 @@ +[ + { + "id": "benchmark-publish-001", + "title": "Clean independent benchmark edge for microscopy segmentation model", + "reviewDate": "2026-08-15", + "model": { + "id": "model-microseg-v4", + "trainingCutoff": "2025-11-01" + }, + "benchmark": { + "id": "benchmark-cellmask-2026", + "releaseDate": "2026-02-15" + }, + "graphEdges": [ + { + "type": "trained_on", + "source": "model-microseg-v4", + "target": "dataset-labcells-2025" + }, + { + "type": "evaluated_on", + "source": "model-microseg-v4", + "target": "benchmark-cellmask-2026", + "doi": "10.5555/cellmask.2026" + } + ], + "splitEvidence": { + "trainingHashes": ["tr-a1", "tr-b2", "tr-c3"], + "evaluationHashes": ["ev-x9", "ev-y8", "ev-z7"], + "trainingLabelSources": ["labcells-curation-v1"], + "benchmarkLabelSources": ["cellmask-independent-v2"], + "independentHoldout": true, + "splitChecksumPresent": true, + "evaluationLockedBeforeTraining": true + }, + "recommendation": { + "publishToEntityPage": true + }, + "safeguards": { + "freezeRecommendations": false + } + }, + { + "id": "benchmark-hold-002", + "title": "Benchmark reused in model pretraining and public recommendation", + "reviewDate": "2026-08-15", + "model": { + "id": "model-proteinrank-7", + "trainingCutoff": "2026-04-01" + }, + "benchmark": { + "id": "benchmark-proteinbind-2025", + "releaseDate": "2025-10-15" + }, + "graphEdges": [ + { + "type": "pretrained_on", + "source": "model-proteinrank-7", + "target": "benchmark-proteinbind-2025", + "datasetId": "benchmark-proteinbind-2025" + }, + { + "type": "claims_sota_on", + "source": "model-proteinrank-7", + "target": "benchmark-proteinbind-2025", + "doi": "10.5555/proteinbind.2025" + } + ], + "splitEvidence": { + "trainingHashes": ["pb-001", "pb-002", "pb-003"], + "evaluationHashes": ["pb-002", "pb-044"], + "trainingLabelSources": ["proteinbind-labels-v1"], + "benchmarkLabelSources": ["proteinbind-labels-v1"], + "independentHoldout": false, + "splitChecksumPresent": true, + "evaluationLockedBeforeTraining": false + }, + "recommendation": { + "publishToEntityPage": true + }, + "safeguards": { + "freezeRecommendations": false + } + }, + { + "id": "benchmark-review-003", + "title": "Older benchmark with missing holdout lock evidence", + "reviewDate": "2026-08-15", + "model": { + "id": "model-solar-forecast-2", + "trainingCutoff": "2026-01-01" + }, + "benchmark": { + "id": "benchmark-solar-2024", + "releaseDate": "2024-07-01" + }, + "graphEdges": [ + { + "type": "trained_on", + "source": "model-solar-forecast-2", + "target": "dataset-grid-2025" + }, + { + "type": "evaluated_on", + "source": "model-solar-forecast-2", + "target": "benchmark-solar-2024" + } + ], + "splitEvidence": { + "trainingHashes": ["grid-1", "grid-2"], + "evaluationHashes": ["solar-a", "solar-b"], + "trainingLabelSources": ["grid-labels"], + "benchmarkLabelSources": ["solar-benchmark-labels"], + "independentHoldout": true, + "splitChecksumPresent": false, + "evaluationLockedBeforeTraining": false + }, + "recommendation": { + "publishToEntityPage": false + }, + "safeguards": { + "freezeRecommendations": true + } + }, + { + "id": "benchmark-hold-004", + "title": "Label source leakage without direct dataset edge", + "reviewDate": "2026-08-15", + "model": { + "id": "model-neuroreader-5", + "trainingCutoff": "2025-08-01" + }, + "benchmark": { + "id": "benchmark-neurocases-2025", + "releaseDate": "2025-12-01" + }, + "graphEdges": [ + { + "type": "fine_tuned_on", + "source": "model-neuroreader-5", + "target": "dataset-hospital-notes-2025" + }, + { + "type": "recommended_for", + "source": "model-neuroreader-5", + "target": "benchmark-neurocases-2025" + } + ], + "splitEvidence": { + "trainingHashes": ["note-1", "note-2"], + "evaluationHashes": ["case-a", "case-b"], + "trainingLabelSources": ["neuro-labeler-shared"], + "benchmarkLabelSources": ["neuro-labeler-shared", "expert-panel-b"], + "independentHoldout": false, + "splitChecksumPresent": true, + "evaluationLockedBeforeTraining": true + }, + "recommendation": { + "publishToEntityPage": true + }, + "safeguards": { + "freezeRecommendations": false + } + } +] diff --git a/benchmark-contamination-graph-guard/package.json b/benchmark-contamination-graph-guard/package.json new file mode 100644 index 00000000..4124afef --- /dev/null +++ b/benchmark-contamination-graph-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "benchmark-contamination-graph-guard", + "version": "1.0.0", + "description": "Dependency-free knowledge graph guard for model benchmark contamination and leakage edges.", + "main": "src/index.js", + "scripts": { + "test": "node --test", + "demo": "node scripts/demo.js", + "video": "node scripts/render-demo-video.js", + "check": "node --check src/index.js && node --check scripts/demo.js && node --check scripts/render-demo-video.js" + }, + "license": "MIT" +} diff --git a/benchmark-contamination-graph-guard/reports/demo.mp4 b/benchmark-contamination-graph-guard/reports/demo.mp4 new file mode 100644 index 00000000..3fdeeb71 Binary files /dev/null and b/benchmark-contamination-graph-guard/reports/demo.mp4 differ diff --git a/benchmark-contamination-graph-guard/reports/reviewer-packet.md b/benchmark-contamination-graph-guard/reports/reviewer-packet.md new file mode 100644 index 00000000..76ef0e18 --- /dev/null +++ b/benchmark-contamination-graph-guard/reports/reviewer-packet.md @@ -0,0 +1,71 @@ +# Benchmark Contamination Graph Guard Report + +Generated: 2026-08-15T21:57:22.692Z +Packets analyzed: 4 +Decision counts: PUBLISH 1, REVIEW 1, HOLD 2 + +## Findings + +### benchmark-publish-001: Clean independent benchmark edge for microscopy segmentation model + +Decision: PUBLISH +Risk score: 0 +Benchmark claim edges: 1 +Contamination edges: 1 + +- No benchmark contamination risk detected. + +Recommended actions: +- Publish benchmark graph edge and recommendation with normal provenance metadata. + +### benchmark-hold-002: Benchmark reused in model pretraining and public recommendation + +Decision: HOLD +Risk score: 100 +Benchmark claim edges: 1 +Contamination edges: 1 + +- CRITICAL DIRECT_TRAINING_BENCHMARK_OVERLAP: The model graph links training or pretraining data directly to the claimed benchmark. +- CRITICAL TRAIN_EVAL_SPLIT_HASH_COLLISION: Training and evaluation split hashes overlap. +- CRITICAL LABEL_SOURCE_LEAKAGE: Training labels and benchmark labels share a source that can leak answers into evaluation. +- MAJOR BENCHMARK_PREDATES_TRAINING_CUTOFF: The benchmark was released before the model training cutoff, increasing contamination risk. +- MAJOR INDEPENDENT_HOLDOUT_EVIDENCE_MISSING: The packet lacks complete independent holdout evidence for the benchmark claim. +- CRITICAL UNSAFE_RECOMMENDATION_PATH_NOT_FROZEN: Entity-page or recommendation publication is enabled while contamination risk is unresolved. + +Recommended actions: +- Freeze model recommendation paths and entity-page benchmark badges. +- Attach contamination findings to the graph curation queue. +- Downgrade or suppress benchmark-confidence badges until evidence is resolved. +- Generate a reviewer packet with split, label-source, and edge-provenance evidence. + +### benchmark-review-003: Older benchmark with missing holdout lock evidence + +Decision: REVIEW +Risk score: 44 +Benchmark claim edges: 1 +Contamination edges: 1 + +- MAJOR BENCHMARK_PREDATES_TRAINING_CUTOFF: The benchmark was released before the model training cutoff, increasing contamination risk. +- MAJOR INDEPENDENT_HOLDOUT_EVIDENCE_MISSING: The packet lacks complete independent holdout evidence for the benchmark claim. + +Recommended actions: +- Attach contamination findings to the graph curation queue. +- Downgrade or suppress benchmark-confidence badges until evidence is resolved. +- Generate a reviewer packet with split, label-source, and edge-provenance evidence. + +### benchmark-hold-004: Label source leakage without direct dataset edge + +Decision: HOLD +Risk score: 92 +Benchmark claim edges: 1 +Contamination edges: 1 + +- CRITICAL LABEL_SOURCE_LEAKAGE: Training labels and benchmark labels share a source that can leak answers into evaluation. +- MAJOR INDEPENDENT_HOLDOUT_EVIDENCE_MISSING: The packet lacks complete independent holdout evidence for the benchmark claim. +- CRITICAL UNSAFE_RECOMMENDATION_PATH_NOT_FROZEN: Entity-page or recommendation publication is enabled while contamination risk is unresolved. + +Recommended actions: +- Freeze model recommendation paths and entity-page benchmark badges. +- Attach contamination findings to the graph curation queue. +- Downgrade or suppress benchmark-confidence badges until evidence is resolved. +- Generate a reviewer packet with split, label-source, and edge-provenance evidence. diff --git a/benchmark-contamination-graph-guard/reports/summary.json b/benchmark-contamination-graph-guard/reports/summary.json new file mode 100644 index 00000000..f2642c00 --- /dev/null +++ b/benchmark-contamination-graph-guard/reports/summary.json @@ -0,0 +1,194 @@ +{ + "generatedAt": "2026-08-15T21:57:22.692Z", + "totalPackets": 4, + "counts": { + "PUBLISH": 1, + "REVIEW": 1, + "HOLD": 2 + }, + "results": [ + { + "id": "benchmark-publish-001", + "title": "Clean independent benchmark edge for microscopy segmentation model", + "decision": "PUBLISH", + "riskScore": 0, + "benchmarkClaimEdges": 1, + "contaminationEdges": 1, + "findings": [], + "recommendedActions": [ + "Publish benchmark graph edge and recommendation with normal provenance metadata." + ] + }, + { + "id": "benchmark-hold-002", + "title": "Benchmark reused in model pretraining and public recommendation", + "decision": "HOLD", + "riskScore": 100, + "benchmarkClaimEdges": 1, + "contaminationEdges": 1, + "findings": [ + { + "code": "DIRECT_TRAINING_BENCHMARK_OVERLAP", + "severity": "critical", + "message": "The model graph links training or pretraining data directly to the claimed benchmark.", + "evidence": { + "model": "model-proteinrank-7", + "benchmark": "benchmark-proteinbind-2025", + "edges": [ + { + "type": "pretrained_on", + "source": "model-proteinrank-7", + "target": "benchmark-proteinbind-2025", + "datasetId": "benchmark-proteinbind-2025" + } + ] + }, + "remediation": "Suppress clean-benchmark claims and mark the evaluation as contaminated until an independent holdout is supplied." + }, + { + "code": "TRAIN_EVAL_SPLIT_HASH_COLLISION", + "severity": "critical", + "message": "Training and evaluation split hashes overlap.", + "evidence": { + "collisions": [ + "pb-002" + ] + }, + "remediation": "Hold the benchmark edge, regenerate split manifests, and require a locked evaluation set before publication." + }, + { + "code": "LABEL_SOURCE_LEAKAGE", + "severity": "critical", + "message": "Training labels and benchmark labels share a source that can leak answers into evaluation.", + "evidence": { + "collisions": [ + "proteinbind-labels-v1" + ] + }, + "remediation": "Block recommendation paths that cite this benchmark as independent evidence until label provenance is separated." + }, + { + "code": "BENCHMARK_PREDATES_TRAINING_CUTOFF", + "severity": "major", + "message": "The benchmark was released before the model training cutoff, increasing contamination risk.", + "evidence": { + "benchmarkReleaseDate": "2025-10-15", + "modelTrainingCutoff": "2026-04-01" + }, + "remediation": "Require explicit exclusion evidence or downgrade the graph edge confidence." + }, + { + "code": "INDEPENDENT_HOLDOUT_EVIDENCE_MISSING", + "severity": "major", + "message": "The packet lacks complete independent holdout evidence for the benchmark claim.", + "evidence": { + "independentHoldout": false, + "splitChecksumPresent": true, + "evaluationLockedBeforeTraining": false + }, + "remediation": "Attach split checksums and locked evaluation-set evidence before publishing entity-page or recommendation claims." + }, + { + "code": "UNSAFE_RECOMMENDATION_PATH_NOT_FROZEN", + "severity": "critical", + "message": "Entity-page or recommendation publication is enabled while contamination risk is unresolved.", + "evidence": { + "publishToEntityPage": true, + "freezeRecommendations": false + }, + "remediation": "Freeze graph recommendation paths and entity-page benchmark badges until contamination review is resolved." + } + ], + "recommendedActions": [ + "Freeze model recommendation paths and entity-page benchmark badges.", + "Attach contamination findings to the graph curation queue.", + "Downgrade or suppress benchmark-confidence badges until evidence is resolved.", + "Generate a reviewer packet with split, label-source, and edge-provenance evidence." + ] + }, + { + "id": "benchmark-review-003", + "title": "Older benchmark with missing holdout lock evidence", + "decision": "REVIEW", + "riskScore": 44, + "benchmarkClaimEdges": 1, + "contaminationEdges": 1, + "findings": [ + { + "code": "BENCHMARK_PREDATES_TRAINING_CUTOFF", + "severity": "major", + "message": "The benchmark was released before the model training cutoff, increasing contamination risk.", + "evidence": { + "benchmarkReleaseDate": "2024-07-01", + "modelTrainingCutoff": "2026-01-01" + }, + "remediation": "Require explicit exclusion evidence or downgrade the graph edge confidence." + }, + { + "code": "INDEPENDENT_HOLDOUT_EVIDENCE_MISSING", + "severity": "major", + "message": "The packet lacks complete independent holdout evidence for the benchmark claim.", + "evidence": { + "independentHoldout": true, + "splitChecksumPresent": false, + "evaluationLockedBeforeTraining": false + }, + "remediation": "Attach split checksums and locked evaluation-set evidence before publishing entity-page or recommendation claims." + } + ], + "recommendedActions": [ + "Attach contamination findings to the graph curation queue.", + "Downgrade or suppress benchmark-confidence badges until evidence is resolved.", + "Generate a reviewer packet with split, label-source, and edge-provenance evidence." + ] + }, + { + "id": "benchmark-hold-004", + "title": "Label source leakage without direct dataset edge", + "decision": "HOLD", + "riskScore": 92, + "benchmarkClaimEdges": 1, + "contaminationEdges": 1, + "findings": [ + { + "code": "LABEL_SOURCE_LEAKAGE", + "severity": "critical", + "message": "Training labels and benchmark labels share a source that can leak answers into evaluation.", + "evidence": { + "collisions": [ + "neuro-labeler-shared" + ] + }, + "remediation": "Block recommendation paths that cite this benchmark as independent evidence until label provenance is separated." + }, + { + "code": "INDEPENDENT_HOLDOUT_EVIDENCE_MISSING", + "severity": "major", + "message": "The packet lacks complete independent holdout evidence for the benchmark claim.", + "evidence": { + "independentHoldout": false, + "splitChecksumPresent": true, + "evaluationLockedBeforeTraining": true + }, + "remediation": "Attach split checksums and locked evaluation-set evidence before publishing entity-page or recommendation claims." + }, + { + "code": "UNSAFE_RECOMMENDATION_PATH_NOT_FROZEN", + "severity": "critical", + "message": "Entity-page or recommendation publication is enabled while contamination risk is unresolved.", + "evidence": { + "publishToEntityPage": true, + "freezeRecommendations": false + }, + "remediation": "Freeze graph recommendation paths and entity-page benchmark badges until contamination review is resolved." + } + ], + "recommendedActions": [ + "Freeze model recommendation paths and entity-page benchmark badges.", + "Attach contamination findings to the graph curation queue.", + "Downgrade or suppress benchmark-confidence badges until evidence is resolved.", + "Generate a reviewer packet with split, label-source, and edge-provenance evidence." + ] + } + ] +} diff --git a/benchmark-contamination-graph-guard/reports/summary.svg b/benchmark-contamination-graph-guard/reports/summary.svg new file mode 100644 index 00000000..59798fb2 --- /dev/null +++ b/benchmark-contamination-graph-guard/reports/summary.svg @@ -0,0 +1,17 @@ + + + Benchmark Contamination Graph Guard + Synthetic model-benchmark graph audit for SCIBASE recommendations. + Decision distribution + HOLD + + 2 + REVIEW + + 1 + PUBLISH + + 1 + Flags direct training overlap, split hash collisions, label leakage, stale benchmarks, and unsafe recommendation paths. + Synthetic generated artifact only. No desktop capture or private data. + diff --git a/benchmark-contamination-graph-guard/scripts/demo.js b/benchmark-contamination-graph-guard/scripts/demo.js new file mode 100644 index 00000000..501e0802 --- /dev/null +++ b/benchmark-contamination-graph-guard/scripts/demo.js @@ -0,0 +1,79 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { analyzeBenchmarkPackets } = require("../src"); + +const root = path.resolve(__dirname, ".."); +const dataPath = path.join(root, "data", "sample_benchmark_graph_packets.json"); +const outDir = path.join(root, "reports"); +const packets = JSON.parse(fs.readFileSync(dataPath, "utf8")); +const report = analyzeBenchmarkPackets(packets); + +fs.mkdirSync(outDir, { recursive: true }); +fs.writeFileSync(path.join(outDir, "summary.json"), `${JSON.stringify(report, null, 2)}\n`); + +const markdown = [ + "# Benchmark Contamination Graph Guard Report", + "", + `Generated: ${report.generatedAt}`, + `Packets analyzed: ${report.totalPackets}`, + `Decision counts: PUBLISH ${report.counts.PUBLISH}, REVIEW ${report.counts.REVIEW}, HOLD ${report.counts.HOLD}`, + "", + "## Findings", + "", + ...report.results.flatMap((item) => [ + `### ${item.id}: ${item.title}`, + "", + `Decision: ${item.decision}`, + `Risk score: ${item.riskScore}`, + `Benchmark claim edges: ${item.benchmarkClaimEdges}`, + `Contamination edges: ${item.contaminationEdges}`, + "", + item.findings.length === 0 + ? "- No benchmark contamination risk detected." + : item.findings.map((finding) => `- ${finding.severity.toUpperCase()} ${finding.code}: ${finding.message}`).join("\n"), + "", + "Recommended actions:", + ...item.recommendedActions.map((action) => `- ${action}`), + "" + ]) +].join("\n"); + +fs.writeFileSync(path.join(outDir, "reviewer-packet.md"), markdown); + +const total = Math.max(1, report.totalPackets); +const maxBar = 620; +const holdWidth = Math.round((report.counts.HOLD / total) * maxBar); +const reviewWidth = Math.round((report.counts.REVIEW / total) * maxBar); +const publishWidth = Math.round((report.counts.PUBLISH / total) * maxBar); +const svg = ` + + Benchmark Contamination Graph Guard + Synthetic model-benchmark graph audit for SCIBASE recommendations. + Decision distribution + HOLD + + ${report.counts.HOLD} + REVIEW + + ${report.counts.REVIEW} + PUBLISH + + ${report.counts.PUBLISH} + Flags direct training overlap, split hash collisions, label leakage, stale benchmarks, and unsafe recommendation paths. + Synthetic generated artifact only. No desktop capture or private data. + +`; + +fs.writeFileSync(path.join(outDir, "summary.svg"), svg); + +console.log(JSON.stringify({ + packets: report.totalPackets, + counts: report.counts, + outputs: [ + path.join(outDir, "summary.json"), + path.join(outDir, "reviewer-packet.md"), + path.join(outDir, "summary.svg") + ] +}, null, 2)); diff --git a/benchmark-contamination-graph-guard/scripts/render-demo-video.js b/benchmark-contamination-graph-guard/scripts/render-demo-video.js new file mode 100644 index 00000000..3ff70751 --- /dev/null +++ b/benchmark-contamination-graph-guard/scripts/render-demo-video.js @@ -0,0 +1,60 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const root = path.resolve(__dirname, ".."); +const ffmpeg = path.resolve( + root, + "..", + "..", + "tool_downloads", + "video_tools", + "node_modules", + "ffmpeg-static", + "ffmpeg.exe" +); +const outDir = path.join(root, "reports"); +const out = path.join(outDir, "demo.mp4"); +const font = "C\\:/Windows/Fonts/arial.ttf"; + +fs.mkdirSync(outDir, { recursive: true }); + +const draw = [ + `drawtext=fontfile=${font}:text='Benchmark Contamination Graph Guard':x=64:y=56:fontsize=39:fontcolor=black`, + `drawtext=fontfile=${font}:text='Model-benchmark leakage audit for scientific knowledge graph recommendations':x=64:y=122:fontsize=24:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='Synthetic graph packets analyzed 4':x=64:y=214:fontsize=34:fontcolor=black`, + `drawtext=fontfile=${font}:text='HOLD 2 REVIEW 1 PUBLISH 1':x=64:y=274:fontsize=40:fontcolor=0x245fd6`, + `drawtext=fontfile=${font}:text='Detects training overlap split hash collisions and label leakage':x=64:y=366:fontsize=26:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='Freezes benchmark badges and recommendation paths until reviewed':x=64:y=428:fontsize=26:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='Synthetic generated slate only no desktop capture or private data':x=64:y=492:fontsize=26:fontcolor=0x34413b` +].join(","); + +if (!fs.existsSync(ffmpeg)) { + throw new Error(`ffmpeg binary not found at ${ffmpeg}`); +} + +const result = spawnSync(ffmpeg, [ + "-y", + "-f", + "lavfi", + "-i", + "color=c=0xf9faf8:s=1280x720:d=8:r=30", + "-vf", + draw, + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + out +], { encoding: "utf8" }); + +if (result.status !== 0) { + process.stderr.write(result.stderr || result.stdout); + process.exit(result.status || 1); +} + +console.log(JSON.stringify({ out, bytes: fs.statSync(out).size }, null, 2)); diff --git a/benchmark-contamination-graph-guard/src/index.js b/benchmark-contamination-graph-guard/src/index.js new file mode 100644 index 00000000..12232b32 --- /dev/null +++ b/benchmark-contamination-graph-guard/src/index.js @@ -0,0 +1,290 @@ +"use strict"; + +const CONTAMINATION_EDGE_TYPES = new Set([ + "trained_on", + "pretrained_on", + "fine_tuned_on", + "label_source", + "derived_from", + "augmented_from", + "leaked_from" +]); + +const BENCHMARK_EDGE_TYPES = new Set([ + "evaluated_on", + "claims_sota_on", + "recommended_for", + "compared_against" +]); + +function list(value) { + return Array.isArray(value) ? value : []; +} + +function normalizeText(value) { + return String(value || "").trim().toLowerCase(); +} + +function parseDate(value) { + const time = Date.parse(value); + return Number.isFinite(time) ? new Date(time) : null; +} + +function daysBetween(startValue, endValue) { + const start = parseDate(startValue); + const end = parseDate(endValue); + if (!start || !end) return null; + return Math.round((end.getTime() - start.getTime()) / 86400000); +} + +function finding(code, severity, message, evidence, remediation) { + return { code, severity, message, evidence, remediation }; +} + +function normalizePacket(packet) { + return { + id: packet.id, + title: packet.title || "Untitled benchmark graph packet", + reviewDate: packet.reviewDate || "2026-08-15", + model: packet.model || {}, + benchmark: packet.benchmark || {}, + graphEdges: list(packet.graphEdges), + splitEvidence: packet.splitEvidence || {}, + recommendation: packet.recommendation || {}, + safeguards: packet.safeguards || {} + }; +} + +function edgeType(edge) { + return normalizeText(edge.type).replace(/\s+/g, "_"); +} + +function sameNode(a, b) { + return normalizeText(a) && normalizeText(a) === normalizeText(b); +} + +function edgeTouches(edge, nodeId) { + return sameNode(edge.source, nodeId) || sameNode(edge.target, nodeId); +} + +function evidenceHashSet(values) { + return new Set(list(values).map(normalizeText).filter(Boolean)); +} + +function intersectSets(left, right) { + return [...left].filter((item) => right.has(item)); +} + +function contaminationEdges(packet) { + const modelId = packet.model.id; + return packet.graphEdges.filter((edge) => ( + edgeTouches(edge, modelId) && + CONTAMINATION_EDGE_TYPES.has(edgeType(edge)) + )); +} + +function benchmarkClaimEdges(packet) { + const benchmarkId = packet.benchmark.id; + return packet.graphEdges.filter((edge) => ( + edgeTouches(edge, benchmarkId) && + BENCHMARK_EDGE_TYPES.has(edgeType(edge)) + )); +} + +function hasDirectTrainingBenchmarkOverlap(packet) { + const benchmarkId = normalizeText(packet.benchmark.id); + return contaminationEdges(packet).some((edge) => ( + sameNode(edge.source, benchmarkId) || + sameNode(edge.target, benchmarkId) || + sameNode(edge.datasetId, benchmarkId) + )); +} + +function splitOverlap(packet) { + const training = evidenceHashSet(packet.splitEvidence.trainingHashes); + const evaluation = evidenceHashSet(packet.splitEvidence.evaluationHashes); + return intersectSets(training, evaluation); +} + +function labelSourceOverlap(packet) { + const trainingLabels = evidenceHashSet(packet.splitEvidence.trainingLabelSources); + const benchmarkLabels = evidenceHashSet(packet.splitEvidence.benchmarkLabelSources); + return intersectSets(trainingLabels, benchmarkLabels); +} + +function benchmarkReleasedAfterTrainingCutoff(packet) { + const trainingCutoff = parseDate(packet.model.trainingCutoff); + const benchmarkRelease = parseDate(packet.benchmark.releaseDate); + if (!trainingCutoff || !benchmarkRelease) return false; + return benchmarkRelease > trainingCutoff; +} + +function hasIndependentHoldoutEvidence(packet) { + return ( + packet.splitEvidence.independentHoldout === true && + packet.splitEvidence.splitChecksumPresent === true && + packet.splitEvidence.evaluationLockedBeforeTraining === true + ); +} + +function scoreFindings(findings) { + const score = findings.reduce((total, item) => { + if (item.severity === "critical") return total + 35; + if (item.severity === "major") return total + 22; + return total + 10; + }, 0); + return Math.min(100, score); +} + +function decisionFor(findings) { + if (findings.some((item) => item.severity === "critical")) return "HOLD"; + if (findings.some((item) => item.severity === "major")) return "REVIEW"; + return "PUBLISH"; +} + +function analyzePacket(input) { + const packet = normalizePacket(input); + const findings = []; + const benchmarkEdges = benchmarkClaimEdges(packet); + const contamination = contaminationEdges(packet); + const splitCollisions = splitOverlap(packet); + const labelCollisions = labelSourceOverlap(packet); + + if (benchmarkEdges.length === 0) { + findings.push(finding( + "NO_BENCHMARK_CLAIM_EDGE", + "major", + "The graph packet lacks an explicit benchmark claim edge for the model evaluation.", + { model: packet.model.id || null, benchmark: packet.benchmark.id || null }, + "Add an evaluated_on or claims_sota_on edge with source DOI and split evidence before recommendation publication." + )); + } + + if (hasDirectTrainingBenchmarkOverlap(packet)) { + findings.push(finding( + "DIRECT_TRAINING_BENCHMARK_OVERLAP", + "critical", + "The model graph links training or pretraining data directly to the claimed benchmark.", + { + model: packet.model.id, + benchmark: packet.benchmark.id, + edges: contamination.map((edge) => ({ type: edge.type, source: edge.source, target: edge.target, datasetId: edge.datasetId || null })) + }, + "Suppress clean-benchmark claims and mark the evaluation as contaminated until an independent holdout is supplied." + )); + } + + if (splitCollisions.length > 0) { + findings.push(finding( + "TRAIN_EVAL_SPLIT_HASH_COLLISION", + "critical", + "Training and evaluation split hashes overlap.", + { collisions: splitCollisions }, + "Hold the benchmark edge, regenerate split manifests, and require a locked evaluation set before publication." + )); + } + + if (labelCollisions.length > 0) { + findings.push(finding( + "LABEL_SOURCE_LEAKAGE", + "critical", + "Training labels and benchmark labels share a source that can leak answers into evaluation.", + { collisions: labelCollisions }, + "Block recommendation paths that cite this benchmark as independent evidence until label provenance is separated." + )); + } + + if (!benchmarkReleasedAfterTrainingCutoff(packet) && packet.benchmark.releaseDate && packet.model.trainingCutoff) { + findings.push(finding( + "BENCHMARK_PREDATES_TRAINING_CUTOFF", + "major", + "The benchmark was released before the model training cutoff, increasing contamination risk.", + { benchmarkReleaseDate: packet.benchmark.releaseDate, modelTrainingCutoff: packet.model.trainingCutoff }, + "Require explicit exclusion evidence or downgrade the graph edge confidence." + )); + } + + if (!hasIndependentHoldoutEvidence(packet)) { + findings.push(finding( + "INDEPENDENT_HOLDOUT_EVIDENCE_MISSING", + "major", + "The packet lacks complete independent holdout evidence for the benchmark claim.", + { + independentHoldout: packet.splitEvidence.independentHoldout || false, + splitChecksumPresent: packet.splitEvidence.splitChecksumPresent || false, + evaluationLockedBeforeTraining: packet.splitEvidence.evaluationLockedBeforeTraining || false + }, + "Attach split checksums and locked evaluation-set evidence before publishing entity-page or recommendation claims." + )); + } + + if ( + findings.length > 0 && + packet.recommendation.publishToEntityPage === true && + packet.safeguards.freezeRecommendations !== true + ) { + findings.push(finding( + "UNSAFE_RECOMMENDATION_PATH_NOT_FROZEN", + "critical", + "Entity-page or recommendation publication is enabled while contamination risk is unresolved.", + { + publishToEntityPage: packet.recommendation.publishToEntityPage, + freezeRecommendations: packet.safeguards.freezeRecommendations || false + }, + "Freeze graph recommendation paths and entity-page benchmark badges until contamination review is resolved." + )); + } + + const decision = decisionFor(findings); + return { + id: packet.id, + title: packet.title, + decision, + riskScore: scoreFindings(findings), + benchmarkClaimEdges: benchmarkEdges.length, + contaminationEdges: contamination.length, + findings, + recommendedActions: buildRecommendedActions(decision, findings) + }; +} + +function buildRecommendedActions(decision, findings) { + if (decision === "PUBLISH") { + return ["Publish benchmark graph edge and recommendation with normal provenance metadata."]; + } + + const actions = [ + "Attach contamination findings to the graph curation queue.", + "Downgrade or suppress benchmark-confidence badges until evidence is resolved.", + "Generate a reviewer packet with split, label-source, and edge-provenance evidence." + ]; + + if (findings.some((item) => item.code === "UNSAFE_RECOMMENDATION_PATH_NOT_FROZEN")) { + actions.unshift("Freeze model recommendation paths and entity-page benchmark badges."); + } + + return actions; +} + +function analyzeBenchmarkPackets(packets) { + const results = list(packets).map(analyzePacket); + const counts = results.reduce((acc, item) => { + acc[item.decision] = (acc[item.decision] || 0) + 1; + return acc; + }, { PUBLISH: 0, REVIEW: 0, HOLD: 0 }); + + return { + generatedAt: new Date().toISOString(), + totalPackets: results.length, + counts, + results + }; +} + +module.exports = { + analyzePacket, + analyzeBenchmarkPackets, + daysBetween, + splitOverlap, + labelSourceOverlap +}; diff --git a/benchmark-contamination-graph-guard/test/benchmark-contamination-graph-guard.test.js b/benchmark-contamination-graph-guard/test/benchmark-contamination-graph-guard.test.js new file mode 100644 index 00000000..0be50495 --- /dev/null +++ b/benchmark-contamination-graph-guard/test/benchmark-contamination-graph-guard.test.js @@ -0,0 +1,65 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); +const { analyzePacket, analyzeBenchmarkPackets, splitOverlap, labelSourceOverlap, daysBetween } = require("../src"); +const samples = require("../data/sample_benchmark_graph_packets.json"); + +test("computes date offsets for graph evidence", () => { + assert.equal(daysBetween("2026-01-01", "2026-01-10"), 9); + assert.equal(daysBetween("bad", "2026-01-10"), null); +}); + +test("detects split hash overlap", () => { + const overlap = splitOverlap({ + splitEvidence: { + trainingHashes: ["A", "B", "C"], + evaluationHashes: ["c", "D"] + } + }); + assert.deepEqual(overlap, ["c"]); +}); + +test("detects label source leakage", () => { + const overlap = labelSourceOverlap({ + splitEvidence: { + trainingLabelSources: ["source-one", "source-two"], + benchmarkLabelSources: ["source-two"] + } + }); + assert.deepEqual(overlap, ["source-two"]); +}); + +test("publishes clean benchmark graph packets", () => { + const result = analyzePacket(samples.find((item) => item.id === "benchmark-publish-001")); + assert.equal(result.decision, "PUBLISH"); + assert.equal(result.findings.length, 0); +}); + +test("holds direct benchmark training contamination", () => { + const result = analyzePacket(samples.find((item) => item.id === "benchmark-hold-002")); + assert.equal(result.decision, "HOLD"); + assert.ok(result.findings.some((item) => item.code === "DIRECT_TRAINING_BENCHMARK_OVERLAP")); + assert.ok(result.findings.some((item) => item.code === "TRAIN_EVAL_SPLIT_HASH_COLLISION")); + assert.ok(result.findings.some((item) => item.code === "LABEL_SOURCE_LEAKAGE")); + assert.ok(result.findings.some((item) => item.code === "UNSAFE_RECOMMENDATION_PATH_NOT_FROZEN")); +}); + +test("routes older benchmarks with missing holdout evidence for review", () => { + const result = analyzePacket(samples.find((item) => item.id === "benchmark-review-003")); + assert.equal(result.decision, "REVIEW"); + assert.ok(result.findings.some((item) => item.code === "BENCHMARK_PREDATES_TRAINING_CUTOFF")); + assert.ok(result.findings.some((item) => item.code === "INDEPENDENT_HOLDOUT_EVIDENCE_MISSING")); +}); + +test("holds label leakage even without direct benchmark training edge", () => { + const result = analyzePacket(samples.find((item) => item.id === "benchmark-hold-004")); + assert.equal(result.decision, "HOLD"); + assert.ok(result.findings.some((item) => item.code === "LABEL_SOURCE_LEAKAGE")); +}); + +test("aggregates benchmark packet decisions", () => { + const report = analyzeBenchmarkPackets(samples); + assert.equal(report.totalPackets, 4); + assert.deepEqual(report.counts, { PUBLISH: 1, REVIEW: 1, HOLD: 2 }); +});