From cca0c5fb1fd303f34cdcfad6ce0aeb6e91dfb305 Mon Sep 17 00:00:00 2001 From: Telemark Codex Agent Date: Sat, 15 Aug 2026 23:58:58 +0200 Subject: [PATCH] Add benchmark contamination graph guard --- benchmark-contamination-graph-guard/README.md | 47 +++ .../data/sample_benchmark_graph_packets.json | 165 ++++++++++ .../package.json | 13 + .../reports/demo.mp4 | Bin 0 -> 54069 bytes .../reports/reviewer-packet.md | 71 +++++ .../reports/summary.json | 194 ++++++++++++ .../reports/summary.svg | 17 + .../scripts/demo.js | 79 +++++ .../scripts/render-demo-video.js | 60 ++++ .../src/index.js | 290 ++++++++++++++++++ ...enchmark-contamination-graph-guard.test.js | 65 ++++ 11 files changed, 1001 insertions(+) create mode 100644 benchmark-contamination-graph-guard/README.md create mode 100644 benchmark-contamination-graph-guard/data/sample_benchmark_graph_packets.json create mode 100644 benchmark-contamination-graph-guard/package.json create mode 100644 benchmark-contamination-graph-guard/reports/demo.mp4 create mode 100644 benchmark-contamination-graph-guard/reports/reviewer-packet.md create mode 100644 benchmark-contamination-graph-guard/reports/summary.json create mode 100644 benchmark-contamination-graph-guard/reports/summary.svg create mode 100644 benchmark-contamination-graph-guard/scripts/demo.js create mode 100644 benchmark-contamination-graph-guard/scripts/render-demo-video.js create mode 100644 benchmark-contamination-graph-guard/src/index.js create mode 100644 benchmark-contamination-graph-guard/test/benchmark-contamination-graph-guard.test.js 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 0000000000000000000000000000000000000000..3fdeeb71cce580cc581c0fb048b8a64af063635c GIT binary patch literal 54069 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 RfcIvxx{Zx2 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 VgnW6M8g=Bi9UeeQ?Dwq4KLB-&s*wNy literal 0 HcmV?d00001 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 }); +});