diff --git a/dataset-split-release-guard/README.md b/dataset-split-release-guard/README.md
new file mode 100644
index 00000000..7df3b1a2
--- /dev/null
+++ b/dataset-split-release-guard/README.md
@@ -0,0 +1,44 @@
+# Dataset Split Release Guard
+
+This contribution adds a focused release guard for train/validation/test split manifests in SCIBASE's Scientific/Engineering Data & Code Hosting layer. It checks whether hosted dataset split artifacts are safe before DOI publication, API release, public previews, or reproducibility badges.
+
+The slice is intentionally narrow. It does not rebuild broad FAIR manifests, artifact package integrity, schema evolution, data dictionaries, malware quarantine, sandbox egress, notebook rerun determinism, spreadsheet formula provenance, accessibility previews, SBOM checks, or general license metadata. It covers the release boundary for split artifacts.
+
+## What It Checks
+
+- Required train, validation, and test split manifests.
+- Dataset record count parity with split record totals.
+- Record identifier overlap across splits.
+- Group-level leakage across participant, patient, subject, lab, or other grouping identifiers.
+- Deterministic seed and split-script hash provenance.
+- Per-split checksum and manifest-hash coverage.
+- License and access-state parity across split artifacts.
+- Class or stratum balance drift beyond release policy.
+- Embargoed records present in public split manifests.
+- Public DOI/API release left enabled while split-integrity findings are unresolved.
+
+## Running Locally
+
+```bash
+npm test
+npm run demo
+npm run check
+npm run video
+```
+
+The demo uses only synthetic split release packets in `data/sample_split_release_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
+
+Data/code hosting is not only about storing files. A hosted dataset often becomes the basis for reproduced results, model comparisons, public APIs, and DOI-backed artifacts. If train, validation, and test manifests leak records or participant groups across boundaries, downstream analyses can look reproducible while being contaminated.
+
+This guard gives SCIBASE a deterministic packet to hold risky releases, regenerate split manifests, preserve checksum evidence, and keep public releases frozen until split artifacts are safe.
diff --git a/dataset-split-release-guard/data/sample_split_release_packets.json b/dataset-split-release-guard/data/sample_split_release_packets.json
new file mode 100644
index 00000000..a12554e1
--- /dev/null
+++ b/dataset-split-release-guard/data/sample_split_release_packets.json
@@ -0,0 +1,214 @@
+[
+ {
+ "id": "split-release-001",
+ "title": "Clean imaging dataset split package for DOI release",
+ "dataset": {
+ "recordCount": 9,
+ "expectedClassBalance": { "case": 0.33, "control": 0.67 }
+ },
+ "splits": {
+ "train": {
+ "recordCount": 5,
+ "recordIds": ["r1", "r2", "r3", "r4", "r5"],
+ "groupIds": ["p1", "p2", "p3", "p4", "p5"],
+ "classBalance": { "case": 0.4, "control": 0.6 },
+ "license": "CC-BY-4.0",
+ "access": "public",
+ "checksum": "sha256:train",
+ "manifestHash": "sha256:manifest-train",
+ "embargoedRecordIds": []
+ },
+ "validation": {
+ "recordCount": 2,
+ "recordIds": ["r6", "r7"],
+ "groupIds": ["p6", "p7"],
+ "classBalance": { "case": 0.5, "control": 0.5 },
+ "license": "CC-BY-4.0",
+ "access": "public",
+ "checksum": "sha256:validation",
+ "manifestHash": "sha256:manifest-validation",
+ "embargoedRecordIds": []
+ },
+ "test": {
+ "recordCount": 2,
+ "recordIds": ["r8", "r9"],
+ "groupIds": ["p8", "p9"],
+ "classBalance": { "case": 0.5, "control": 0.5 },
+ "license": "CC-BY-4.0",
+ "access": "public",
+ "checksum": "sha256:test",
+ "manifestHash": "sha256:manifest-test",
+ "embargoedRecordIds": []
+ }
+ },
+ "provenance": {
+ "seedLocked": true,
+ "splitScriptHashPresent": true
+ },
+ "release": {
+ "publicDoiRelease": true,
+ "freezePublicRelease": false
+ },
+ "policy": {
+ "requireGroupDisjointness": true,
+ "maxClassBalanceDelta": 0.2
+ }
+ },
+ {
+ "id": "split-hold-002",
+ "title": "Clinical split package with patient leakage and embargoed rows",
+ "dataset": {
+ "recordCount": 8,
+ "expectedClassBalance": { "case": 0.5, "control": 0.5 }
+ },
+ "splits": {
+ "train": {
+ "recordCount": 4,
+ "recordIds": ["c1", "c2", "c3", "c4"],
+ "groupIds": ["patient-1", "patient-2", "patient-3", "patient-4"],
+ "classBalance": { "case": 0.75, "control": 0.25 },
+ "license": "restricted-clinical",
+ "access": "private",
+ "checksum": "sha256:train",
+ "manifestHash": "sha256:manifest-train",
+ "embargoedRecordIds": []
+ },
+ "validation": {
+ "recordCount": 2,
+ "recordIds": ["c4", "c5"],
+ "groupIds": ["patient-4", "patient-5"],
+ "classBalance": { "case": 1,
+ "control": 0 },
+ "license": "restricted-clinical",
+ "access": "public",
+ "checksum": "",
+ "manifestHash": "",
+ "embargoedRecordIds": ["c5"]
+ },
+ "test": {
+ "recordCount": 2,
+ "recordIds": ["c6", "c7"],
+ "groupIds": ["patient-6", "patient-7"],
+ "classBalance": { "case": 0,
+ "control": 1 },
+ "license": "restricted-clinical",
+ "access": "private",
+ "checksum": "sha256:test",
+ "manifestHash": "sha256:manifest-test",
+ "embargoedRecordIds": []
+ }
+ },
+ "provenance": {
+ "seedLocked": false,
+ "splitScriptHashPresent": false
+ },
+ "release": {
+ "publicDoiRelease": true,
+ "freezePublicRelease": false
+ },
+ "policy": {
+ "requireGroupDisjointness": true,
+ "maxClassBalanceDelta": 0.15
+ }
+ },
+ {
+ "id": "split-review-003",
+ "title": "Good disjointness with missing provenance and balance drift",
+ "dataset": {
+ "recordCount": 6,
+ "expectedClassBalance": { "positive": 0.5, "negative": 0.5 }
+ },
+ "splits": {
+ "train": {
+ "recordCount": 3,
+ "recordIds": ["a1", "a2", "a3"],
+ "groupIds": ["g1", "g2", "g3"],
+ "classBalance": { "positive": 0.9, "negative": 0.1 },
+ "license": "CC0-1.0",
+ "access": "public",
+ "checksum": "sha256:train",
+ "manifestHash": "sha256:manifest-train",
+ "embargoedRecordIds": []
+ },
+ "validation": {
+ "recordCount": 1,
+ "recordIds": ["a4"],
+ "groupIds": ["g4"],
+ "classBalance": { "positive": 0,
+ "negative": 1 },
+ "license": "CC0-1.0",
+ "access": "public",
+ "checksum": "sha256:validation",
+ "manifestHash": "sha256:manifest-validation",
+ "embargoedRecordIds": []
+ },
+ "test": {
+ "recordCount": 2,
+ "recordIds": ["a5", "a6"],
+ "groupIds": ["g5", "g6"],
+ "classBalance": { "positive": 0.5, "negative": 0.5 },
+ "license": "CC0-1.0",
+ "access": "public",
+ "checksum": "sha256:test",
+ "manifestHash": "sha256:manifest-test",
+ "embargoedRecordIds": []
+ }
+ },
+ "provenance": {
+ "seedLocked": true,
+ "splitScriptHashPresent": false
+ },
+ "release": {
+ "publicDoiRelease": false,
+ "freezePublicRelease": true
+ },
+ "policy": {
+ "requireGroupDisjointness": true,
+ "maxClassBalanceDelta": 0.25
+ }
+ },
+ {
+ "id": "split-hold-004",
+ "title": "Missing test split with public release still enabled",
+ "dataset": {
+ "recordCount": 5,
+ "expectedClassBalance": { "included": 1 }
+ },
+ "splits": {
+ "train": {
+ "recordCount": 3,
+ "recordIds": ["m1", "m2", "m3"],
+ "groupIds": ["mg1", "mg2", "mg3"],
+ "classBalance": { "included": 1 },
+ "license": "CC-BY-4.0",
+ "access": "public",
+ "checksum": "sha256:train",
+ "manifestHash": "sha256:manifest-train",
+ "embargoedRecordIds": []
+ },
+ "validation": {
+ "recordCount": 2,
+ "recordIds": ["m4", "m5"],
+ "groupIds": ["mg4", "mg5"],
+ "classBalance": { "included": 1 },
+ "license": "CC-BY-4.0",
+ "access": "public",
+ "checksum": "sha256:validation",
+ "manifestHash": "sha256:manifest-validation",
+ "embargoedRecordIds": []
+ }
+ },
+ "provenance": {
+ "seedLocked": true,
+ "splitScriptHashPresent": true
+ },
+ "release": {
+ "publicDoiRelease": true,
+ "freezePublicRelease": false
+ },
+ "policy": {
+ "requireGroupDisjointness": true,
+ "maxClassBalanceDelta": 0.15
+ }
+ }
+]
diff --git a/dataset-split-release-guard/package.json b/dataset-split-release-guard/package.json
new file mode 100644
index 00000000..d3562a30
--- /dev/null
+++ b/dataset-split-release-guard/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "dataset-split-release-guard",
+ "version": "1.0.0",
+ "description": "Dependency-free release guard for hosted train/validation/test split manifests.",
+ "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/dataset-split-release-guard/reports/demo.mp4 b/dataset-split-release-guard/reports/demo.mp4
new file mode 100644
index 00000000..7574a7c2
Binary files /dev/null and b/dataset-split-release-guard/reports/demo.mp4 differ
diff --git a/dataset-split-release-guard/reports/reviewer-packet.md b/dataset-split-release-guard/reports/reviewer-packet.md
new file mode 100644
index 00000000..9b3b6682
--- /dev/null
+++ b/dataset-split-release-guard/reports/reviewer-packet.md
@@ -0,0 +1,64 @@
+# Dataset Split Release Guard Report
+
+Generated: 2026-08-15T22:12:47.311Z
+Packets analyzed: 4
+Decision counts: RELEASE 1, REVIEW_RELEASE 1, HOLD_RELEASE 2
+
+## Findings
+
+### split-release-001: Clean imaging dataset split package for DOI release
+
+Decision: RELEASE
+Splits: train, validation, test
+
+- No split release risk detected.
+
+Recommended actions:
+- Release split manifests with normal artifact provenance.
+
+### split-hold-002: Clinical split package with patient leakage and embargoed rows
+
+Decision: HOLD_RELEASE
+Splits: train, validation, test
+
+- CRITICAL TRAIN_VALIDATION_TEST_RECORD_OVERLAP: One or more records appear in multiple splits.
+- CRITICAL GROUP_LEAKAGE_ACROSS_SPLITS: A participant, patient, subject, lab, or other grouping identifier appears across multiple splits.
+- MAJOR SPLIT_PROVENANCE_INCOMPLETE: The split generation seed or script hash is missing.
+- MAJOR SPLIT_CHECKSUM_COVERAGE_MISSING: One or more split manifests lack checksum or manifest hash evidence.
+- MAJOR SPLIT_LICENSE_ACCESS_PARITY_MISMATCH: Split manifests have inconsistent license or access states.
+- MAJOR STRATIFICATION_BALANCE_DRIFT: Class or stratum balance differs from the dataset policy beyond the allowed threshold.
+- CRITICAL EMBARGOED_RECORD_IN_PUBLIC_SPLIT: Embargoed records are present in one or more release split manifests.
+- CRITICAL PUBLIC_RELEASE_NOT_FROZEN_FOR_SPLIT_RISK: Public DOI/API release is enabled while split-integrity findings remain unresolved.
+
+Recommended actions:
+- Freeze public DOI/API release and reproducibility badges.
+- Attach findings to the dataset release review packet.
+- Regenerate split manifests or document curator-approved exceptions.
+- Re-run checksum, license, access, and stratification checks before DOI/API release.
+
+### split-review-003: Good disjointness with missing provenance and balance drift
+
+Decision: REVIEW_RELEASE
+Splits: train, validation, test
+
+- MAJOR SPLIT_PROVENANCE_INCOMPLETE: The split generation seed or script hash is missing.
+- MAJOR STRATIFICATION_BALANCE_DRIFT: Class or stratum balance differs from the dataset policy beyond the allowed threshold.
+
+Recommended actions:
+- Attach findings to the dataset release review packet.
+- Regenerate split manifests or document curator-approved exceptions.
+- Re-run checksum, license, access, and stratification checks before DOI/API release.
+
+### split-hold-004: Missing test split with public release still enabled
+
+Decision: HOLD_RELEASE
+Splits: train, validation
+
+- CRITICAL REQUIRED_SPLIT_MISSING: The release packet lacks one or more required train/validation/test split manifests.
+- CRITICAL PUBLIC_RELEASE_NOT_FROZEN_FOR_SPLIT_RISK: Public DOI/API release is enabled while split-integrity findings remain unresolved.
+
+Recommended actions:
+- Freeze public DOI/API release and reproducibility badges.
+- Attach findings to the dataset release review packet.
+- Regenerate split manifests or document curator-approved exceptions.
+- Re-run checksum, license, access, and stratification checks before DOI/API release.
diff --git a/dataset-split-release-guard/reports/summary.json b/dataset-split-release-guard/reports/summary.json
new file mode 100644
index 00000000..df142cf1
--- /dev/null
+++ b/dataset-split-release-guard/reports/summary.json
@@ -0,0 +1,317 @@
+{
+ "generatedAt": "2026-08-15T22:12:47.311Z",
+ "totalPackets": 4,
+ "counts": {
+ "RELEASE": 1,
+ "REVIEW_RELEASE": 1,
+ "HOLD_RELEASE": 2
+ },
+ "results": [
+ {
+ "id": "split-release-001",
+ "title": "Clean imaging dataset split package for DOI release",
+ "decision": "RELEASE",
+ "splitNames": [
+ "train",
+ "validation",
+ "test"
+ ],
+ "findings": [],
+ "recommendedActions": [
+ "Release split manifests with normal artifact provenance."
+ ]
+ },
+ {
+ "id": "split-hold-002",
+ "title": "Clinical split package with patient leakage and embargoed rows",
+ "decision": "HOLD_RELEASE",
+ "splitNames": [
+ "train",
+ "validation",
+ "test"
+ ],
+ "findings": [
+ {
+ "code": "TRAIN_VALIDATION_TEST_RECORD_OVERLAP",
+ "severity": "critical",
+ "message": "One or more records appear in multiple splits.",
+ "evidence": {
+ "overlaps": [
+ {
+ "left": "train",
+ "right": "validation",
+ "key": "recordIds",
+ "values": [
+ "c4"
+ ]
+ }
+ ]
+ },
+ "remediation": "Regenerate split manifests with disjoint record identifiers and new manifest hashes."
+ },
+ {
+ "code": "GROUP_LEAKAGE_ACROSS_SPLITS",
+ "severity": "critical",
+ "message": "A participant, patient, subject, lab, or other grouping identifier appears across multiple splits.",
+ "evidence": {
+ "overlaps": [
+ {
+ "left": "train",
+ "right": "validation",
+ "key": "groupIds",
+ "values": [
+ "patient-4"
+ ]
+ }
+ ]
+ },
+ "remediation": "Regenerate splits with group-level disjointness to prevent leakage across train/validation/test boundaries."
+ },
+ {
+ "code": "SPLIT_PROVENANCE_INCOMPLETE",
+ "severity": "major",
+ "message": "The split generation seed or script hash is missing.",
+ "evidence": {
+ "seedLocked": false,
+ "splitScriptHashPresent": false
+ },
+ "remediation": "Attach deterministic seed and split-script hash evidence before DOI/API release."
+ },
+ {
+ "code": "SPLIT_CHECKSUM_COVERAGE_MISSING",
+ "severity": "major",
+ "message": "One or more split manifests lack checksum or manifest hash evidence.",
+ "evidence": {
+ "splits": [
+ "train",
+ "validation",
+ "test"
+ ]
+ },
+ "remediation": "Add per-split content checksums and manifest hashes before release."
+ },
+ {
+ "code": "SPLIT_LICENSE_ACCESS_PARITY_MISMATCH",
+ "severity": "major",
+ "message": "Split manifests have inconsistent license or access states.",
+ "evidence": {
+ "licenses": [
+ {
+ "split": "train",
+ "license": "restricted-clinical"
+ },
+ {
+ "split": "validation",
+ "license": "restricted-clinical"
+ },
+ {
+ "split": "test",
+ "license": "restricted-clinical"
+ }
+ ],
+ "access": [
+ {
+ "split": "train",
+ "access": "private"
+ },
+ {
+ "split": "validation",
+ "access": "public"
+ },
+ {
+ "split": "test",
+ "access": "private"
+ }
+ ]
+ },
+ "remediation": "Align license and access state across splits or document why a split must remain restricted."
+ },
+ {
+ "code": "STRATIFICATION_BALANCE_DRIFT",
+ "severity": "major",
+ "message": "Class or stratum balance differs from the dataset policy beyond the allowed threshold.",
+ "evidence": {
+ "drifts": [
+ {
+ "split": "train",
+ "label": "case",
+ "actual": 0.75,
+ "expected": 0.5,
+ "delta": 0.25
+ },
+ {
+ "split": "train",
+ "label": "control",
+ "actual": 0.25,
+ "expected": 0.5,
+ "delta": 0.25
+ },
+ {
+ "split": "validation",
+ "label": "case",
+ "actual": 1,
+ "expected": 0.5,
+ "delta": 0.5
+ },
+ {
+ "split": "validation",
+ "label": "control",
+ "actual": 0,
+ "expected": 0.5,
+ "delta": 0.5
+ },
+ {
+ "split": "test",
+ "label": "case",
+ "actual": 0,
+ "expected": 0.5,
+ "delta": 0.5
+ },
+ {
+ "split": "test",
+ "label": "control",
+ "actual": 1,
+ "expected": 0.5,
+ "delta": 0.5
+ }
+ ]
+ },
+ "remediation": "Regenerate stratified splits or document the imbalance in the release notes."
+ },
+ {
+ "code": "EMBARGOED_RECORD_IN_PUBLIC_SPLIT",
+ "severity": "critical",
+ "message": "Embargoed records are present in one or more release split manifests.",
+ "evidence": {
+ "embargoedRecords": [
+ {
+ "split": "validation",
+ "recordId": "c5"
+ }
+ ]
+ },
+ "remediation": "Remove embargoed records or keep the affected split private until embargo restrictions are lifted."
+ },
+ {
+ "code": "PUBLIC_RELEASE_NOT_FROZEN_FOR_SPLIT_RISK",
+ "severity": "critical",
+ "message": "Public DOI/API release is enabled while split-integrity findings remain unresolved.",
+ "evidence": {
+ "publicDoiRelease": true,
+ "freezePublicRelease": false
+ },
+ "remediation": "Freeze DOI/API/public-preview release until split integrity issues are resolved."
+ }
+ ],
+ "recommendedActions": [
+ "Freeze public DOI/API release and reproducibility badges.",
+ "Attach findings to the dataset release review packet.",
+ "Regenerate split manifests or document curator-approved exceptions.",
+ "Re-run checksum, license, access, and stratification checks before DOI/API release."
+ ]
+ },
+ {
+ "id": "split-review-003",
+ "title": "Good disjointness with missing provenance and balance drift",
+ "decision": "REVIEW_RELEASE",
+ "splitNames": [
+ "train",
+ "validation",
+ "test"
+ ],
+ "findings": [
+ {
+ "code": "SPLIT_PROVENANCE_INCOMPLETE",
+ "severity": "major",
+ "message": "The split generation seed or script hash is missing.",
+ "evidence": {
+ "seedLocked": true,
+ "splitScriptHashPresent": false
+ },
+ "remediation": "Attach deterministic seed and split-script hash evidence before DOI/API release."
+ },
+ {
+ "code": "STRATIFICATION_BALANCE_DRIFT",
+ "severity": "major",
+ "message": "Class or stratum balance differs from the dataset policy beyond the allowed threshold.",
+ "evidence": {
+ "drifts": [
+ {
+ "split": "train",
+ "label": "positive",
+ "actual": 0.9,
+ "expected": 0.5,
+ "delta": 0.4
+ },
+ {
+ "split": "train",
+ "label": "negative",
+ "actual": 0.1,
+ "expected": 0.5,
+ "delta": 0.4
+ },
+ {
+ "split": "validation",
+ "label": "positive",
+ "actual": 0,
+ "expected": 0.5,
+ "delta": 0.5
+ },
+ {
+ "split": "validation",
+ "label": "negative",
+ "actual": 1,
+ "expected": 0.5,
+ "delta": 0.5
+ }
+ ]
+ },
+ "remediation": "Regenerate stratified splits or document the imbalance in the release notes."
+ }
+ ],
+ "recommendedActions": [
+ "Attach findings to the dataset release review packet.",
+ "Regenerate split manifests or document curator-approved exceptions.",
+ "Re-run checksum, license, access, and stratification checks before DOI/API release."
+ ]
+ },
+ {
+ "id": "split-hold-004",
+ "title": "Missing test split with public release still enabled",
+ "decision": "HOLD_RELEASE",
+ "splitNames": [
+ "train",
+ "validation"
+ ],
+ "findings": [
+ {
+ "code": "REQUIRED_SPLIT_MISSING",
+ "severity": "critical",
+ "message": "The release packet lacks one or more required train/validation/test split manifests.",
+ "evidence": {
+ "missingSplits": [
+ "test"
+ ]
+ },
+ "remediation": "Hold release until all required split manifests are present and checksummed."
+ },
+ {
+ "code": "PUBLIC_RELEASE_NOT_FROZEN_FOR_SPLIT_RISK",
+ "severity": "critical",
+ "message": "Public DOI/API release is enabled while split-integrity findings remain unresolved.",
+ "evidence": {
+ "publicDoiRelease": true,
+ "freezePublicRelease": false
+ },
+ "remediation": "Freeze DOI/API/public-preview release until split integrity issues are resolved."
+ }
+ ],
+ "recommendedActions": [
+ "Freeze public DOI/API release and reproducibility badges.",
+ "Attach findings to the dataset release review packet.",
+ "Regenerate split manifests or document curator-approved exceptions.",
+ "Re-run checksum, license, access, and stratification checks before DOI/API release."
+ ]
+ }
+ ]
+}
diff --git a/dataset-split-release-guard/reports/summary.svg b/dataset-split-release-guard/reports/summary.svg
new file mode 100644
index 00000000..327ebfd7
--- /dev/null
+++ b/dataset-split-release-guard/reports/summary.svg
@@ -0,0 +1,17 @@
+
diff --git a/dataset-split-release-guard/scripts/demo.js b/dataset-split-release-guard/scripts/demo.js
new file mode 100644
index 00000000..3df0b3d1
--- /dev/null
+++ b/dataset-split-release-guard/scripts/demo.js
@@ -0,0 +1,77 @@
+"use strict";
+
+const fs = require("node:fs");
+const path = require("node:path");
+const { analyzeSplitReleasePackets } = require("../src");
+
+const root = path.resolve(__dirname, "..");
+const dataPath = path.join(root, "data", "sample_split_release_packets.json");
+const outDir = path.join(root, "reports");
+const packets = JSON.parse(fs.readFileSync(dataPath, "utf8"));
+const report = analyzeSplitReleasePackets(packets);
+
+fs.mkdirSync(outDir, { recursive: true });
+fs.writeFileSync(path.join(outDir, "summary.json"), `${JSON.stringify(report, null, 2)}\n`);
+
+const markdown = [
+ "# Dataset Split Release Guard Report",
+ "",
+ `Generated: ${report.generatedAt}`,
+ `Packets analyzed: ${report.totalPackets}`,
+ `Decision counts: RELEASE ${report.counts.RELEASE}, REVIEW_RELEASE ${report.counts.REVIEW_RELEASE}, HOLD_RELEASE ${report.counts.HOLD_RELEASE}`,
+ "",
+ "## Findings",
+ "",
+ ...report.results.flatMap((item) => [
+ `### ${item.id}: ${item.title}`,
+ "",
+ `Decision: ${item.decision}`,
+ `Splits: ${item.splitNames.join(", ")}`,
+ "",
+ item.findings.length === 0
+ ? "- No split release 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_RELEASE / total) * maxBar);
+const reviewWidth = Math.round((report.counts.REVIEW_RELEASE / total) * maxBar);
+const releaseWidth = Math.round((report.counts.RELEASE / total) * maxBar);
+const svg = `
+`;
+
+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/dataset-split-release-guard/scripts/render-demo-video.js b/dataset-split-release-guard/scripts/render-demo-video.js
new file mode 100644
index 00000000..93702374
--- /dev/null
+++ b/dataset-split-release-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='Dataset Split Release Guard':x=64:y=56:fontsize=42:fontcolor=black`,
+ `drawtext=fontfile=${font}:text='Train validation test manifest audit before public hosting release':x=64:y=122:fontsize=25:fontcolor=0x34413b`,
+ `drawtext=fontfile=${font}:text='Synthetic split packets analyzed 4':x=64:y=214:fontsize=34:fontcolor=black`,
+ `drawtext=fontfile=${font}:text='HOLD 2 REVIEW 1 RELEASE 1':x=64:y=274:fontsize=40:fontcolor=0x245fd6`,
+ `drawtext=fontfile=${font}:text='Detects record overlap group leakage embargo rows and checksum gaps':x=64:y=366:fontsize=26:fontcolor=0x34413b`,
+ `drawtext=fontfile=${font}:text='Freezes DOI API release until split artifacts are safe':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/dataset-split-release-guard/src/index.js b/dataset-split-release-guard/src/index.js
new file mode 100644
index 00000000..7f2b9bec
--- /dev/null
+++ b/dataset-split-release-guard/src/index.js
@@ -0,0 +1,275 @@
+"use strict";
+
+const REQUIRED_SPLITS = ["train", "validation", "test"];
+
+function list(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function normalizeText(value) {
+ return String(value || "").trim().toLowerCase();
+}
+
+function finding(code, severity, message, evidence, remediation) {
+ return { code, severity, message, evidence, remediation };
+}
+
+function normalizePacket(packet) {
+ return {
+ id: packet.id,
+ title: packet.title || "Untitled split release packet",
+ dataset: packet.dataset || {},
+ splits: packet.splits || {},
+ provenance: packet.provenance || {},
+ release: packet.release || {},
+ policy: packet.policy || {}
+ };
+}
+
+function splitNames(packet) {
+ return Object.keys(packet.splits || {}).map(normalizeText);
+}
+
+function missingRequiredSplits(packet) {
+ const present = new Set(splitNames(packet));
+ return REQUIRED_SPLITS.filter((name) => !present.has(name));
+}
+
+function idsForSplit(packet, splitName, key) {
+ return new Set(list(packet.splits[splitName]?.[key]).map(normalizeText).filter(Boolean));
+}
+
+function intersect(left, right) {
+ return [...left].filter((item) => right.has(item));
+}
+
+function pairwiseOverlaps(packet, key) {
+ const names = splitNames(packet);
+ const overlaps = [];
+ for (let i = 0; i < names.length; i += 1) {
+ for (let j = i + 1; j < names.length; j += 1) {
+ const a = names[i];
+ const b = names[j];
+ const values = intersect(idsForSplit(packet, a, key), idsForSplit(packet, b, key));
+ if (values.length > 0) overlaps.push({ left: a, right: b, key, values });
+ }
+ }
+ return overlaps;
+}
+
+function allSplitRecords(packet) {
+ return splitNames(packet).reduce((total, name) => total + Number(packet.splits[name]?.recordCount || 0), 0);
+}
+
+function hasChecksumCoverage(packet) {
+ return splitNames(packet).every((name) => packet.splits[name]?.checksum && packet.splits[name]?.manifestHash);
+}
+
+function hasLicenseAccessParity(packet) {
+ const licenses = new Set(splitNames(packet).map((name) => normalizeText(packet.splits[name]?.license)).filter(Boolean));
+ const access = new Set(splitNames(packet).map((name) => normalizeText(packet.splits[name]?.access)).filter(Boolean));
+ return licenses.size <= 1 && access.size <= 1 && licenses.size > 0 && access.size > 0;
+}
+
+function classBalanceDrift(packet) {
+ const names = splitNames(packet);
+ const target = packet.policy.maxClassBalanceDelta ?? 0.15;
+ const drifts = [];
+ for (const name of names) {
+ const balance = packet.splits[name]?.classBalance || {};
+ for (const [label, value] of Object.entries(balance)) {
+ const expected = Number(packet.dataset.expectedClassBalance?.[label]);
+ if (Number.isFinite(expected) && Math.abs(Number(value) - expected) > target) {
+ drifts.push({ split: name, label, actual: Number(value), expected, delta: Number(Math.abs(Number(value) - expected).toFixed(3)) });
+ }
+ }
+ }
+ return drifts;
+}
+
+function embargoViolations(packet) {
+ return splitNames(packet).flatMap((name) => {
+ const rows = list(packet.splits[name]?.embargoedRecordIds);
+ return rows.map((recordId) => ({ split: name, recordId }));
+ });
+}
+
+function analyzePacket(input) {
+ const packet = normalizePacket(input);
+ const findings = [];
+ const missing = missingRequiredSplits(packet);
+ const recordOverlaps = pairwiseOverlaps(packet, "recordIds");
+ const groupOverlaps = pairwiseOverlaps(packet, "groupIds");
+ const drifts = classBalanceDrift(packet);
+ const embargoed = embargoViolations(packet);
+
+ if (missing.length > 0) {
+ findings.push(finding(
+ "REQUIRED_SPLIT_MISSING",
+ "critical",
+ "The release packet lacks one or more required train/validation/test split manifests.",
+ { missingSplits: missing },
+ "Hold release until all required split manifests are present and checksummed."
+ ));
+ }
+
+ if (allSplitRecords(packet) !== Number(packet.dataset.recordCount || 0)) {
+ findings.push(finding(
+ "SPLIT_RECORD_TOTAL_MISMATCH",
+ "major",
+ "The split record totals do not match the dataset record count.",
+ { datasetRecordCount: Number(packet.dataset.recordCount || 0), splitRecordTotal: allSplitRecords(packet) },
+ "Reconcile split manifests against the registered dataset row count before release."
+ ));
+ }
+
+ if (recordOverlaps.length > 0) {
+ findings.push(finding(
+ "TRAIN_VALIDATION_TEST_RECORD_OVERLAP",
+ "critical",
+ "One or more records appear in multiple splits.",
+ { overlaps: recordOverlaps },
+ "Regenerate split manifests with disjoint record identifiers and new manifest hashes."
+ ));
+ }
+
+ if (groupOverlaps.length > 0 && packet.policy.requireGroupDisjointness !== false) {
+ findings.push(finding(
+ "GROUP_LEAKAGE_ACROSS_SPLITS",
+ "critical",
+ "A participant, patient, subject, lab, or other grouping identifier appears across multiple splits.",
+ { overlaps: groupOverlaps },
+ "Regenerate splits with group-level disjointness to prevent leakage across train/validation/test boundaries."
+ ));
+ }
+
+ if (packet.provenance.seedLocked !== true || packet.provenance.splitScriptHashPresent !== true) {
+ findings.push(finding(
+ "SPLIT_PROVENANCE_INCOMPLETE",
+ "major",
+ "The split generation seed or script hash is missing.",
+ {
+ seedLocked: packet.provenance.seedLocked || false,
+ splitScriptHashPresent: packet.provenance.splitScriptHashPresent || false
+ },
+ "Attach deterministic seed and split-script hash evidence before DOI/API release."
+ ));
+ }
+
+ if (!hasChecksumCoverage(packet)) {
+ findings.push(finding(
+ "SPLIT_CHECKSUM_COVERAGE_MISSING",
+ "major",
+ "One or more split manifests lack checksum or manifest hash evidence.",
+ { splits: splitNames(packet) },
+ "Add per-split content checksums and manifest hashes before release."
+ ));
+ }
+
+ if (!hasLicenseAccessParity(packet)) {
+ findings.push(finding(
+ "SPLIT_LICENSE_ACCESS_PARITY_MISMATCH",
+ "major",
+ "Split manifests have inconsistent license or access states.",
+ {
+ licenses: splitNames(packet).map((name) => ({ split: name, license: packet.splits[name]?.license || null })),
+ access: splitNames(packet).map((name) => ({ split: name, access: packet.splits[name]?.access || null }))
+ },
+ "Align license and access state across splits or document why a split must remain restricted."
+ ));
+ }
+
+ if (drifts.length > 0) {
+ findings.push(finding(
+ "STRATIFICATION_BALANCE_DRIFT",
+ "major",
+ "Class or stratum balance differs from the dataset policy beyond the allowed threshold.",
+ { drifts },
+ "Regenerate stratified splits or document the imbalance in the release notes."
+ ));
+ }
+
+ if (embargoed.length > 0) {
+ findings.push(finding(
+ "EMBARGOED_RECORD_IN_PUBLIC_SPLIT",
+ "critical",
+ "Embargoed records are present in one or more release split manifests.",
+ { embargoedRecords: embargoed },
+ "Remove embargoed records or keep the affected split private until embargo restrictions are lifted."
+ ));
+ }
+
+ if (
+ findings.length > 0 &&
+ packet.release.publicDoiRelease === true &&
+ packet.release.freezePublicRelease !== true
+ ) {
+ findings.push(finding(
+ "PUBLIC_RELEASE_NOT_FROZEN_FOR_SPLIT_RISK",
+ "critical",
+ "Public DOI/API release is enabled while split-integrity findings remain unresolved.",
+ {
+ publicDoiRelease: packet.release.publicDoiRelease,
+ freezePublicRelease: packet.release.freezePublicRelease || false
+ },
+ "Freeze DOI/API/public-preview release until split integrity issues are resolved."
+ ));
+ }
+
+ const decision = decisionFor(findings);
+ return {
+ id: packet.id,
+ title: packet.title,
+ decision,
+ splitNames: splitNames(packet),
+ findings,
+ recommendedActions: buildRecommendedActions(decision, findings)
+ };
+}
+
+function decisionFor(findings) {
+ if (findings.some((item) => item.severity === "critical")) return "HOLD_RELEASE";
+ if (findings.some((item) => item.severity === "major")) return "REVIEW_RELEASE";
+ return "RELEASE";
+}
+
+function buildRecommendedActions(decision, findings) {
+ if (decision === "RELEASE") {
+ return ["Release split manifests with normal artifact provenance."];
+ }
+
+ const actions = [
+ "Attach findings to the dataset release review packet.",
+ "Regenerate split manifests or document curator-approved exceptions.",
+ "Re-run checksum, license, access, and stratification checks before DOI/API release."
+ ];
+
+ if (findings.some((item) => item.code === "PUBLIC_RELEASE_NOT_FROZEN_FOR_SPLIT_RISK")) {
+ actions.unshift("Freeze public DOI/API release and reproducibility badges.");
+ }
+
+ return actions;
+}
+
+function analyzeSplitReleasePackets(packets) {
+ const results = list(packets).map(analyzePacket);
+ const counts = results.reduce((acc, item) => {
+ acc[item.decision] = (acc[item.decision] || 0) + 1;
+ return acc;
+ }, { RELEASE: 0, REVIEW_RELEASE: 0, HOLD_RELEASE: 0 });
+
+ return {
+ generatedAt: new Date().toISOString(),
+ totalPackets: results.length,
+ counts,
+ results
+ };
+}
+
+module.exports = {
+ analyzePacket,
+ analyzeSplitReleasePackets,
+ missingRequiredSplits,
+ pairwiseOverlaps,
+ classBalanceDrift
+};
diff --git a/dataset-split-release-guard/test/dataset-split-release-guard.test.js b/dataset-split-release-guard/test/dataset-split-release-guard.test.js
new file mode 100644
index 00000000..61798737
--- /dev/null
+++ b/dataset-split-release-guard/test/dataset-split-release-guard.test.js
@@ -0,0 +1,56 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const test = require("node:test");
+const { analyzePacket, analyzeSplitReleasePackets, missingRequiredSplits, pairwiseOverlaps, classBalanceDrift } = require("../src");
+const samples = require("../data/sample_split_release_packets.json");
+
+test("detects missing required split manifests", () => {
+ const missing = missingRequiredSplits(samples.find((item) => item.id === "split-hold-004"));
+ assert.deepEqual(missing, ["test"]);
+});
+
+test("detects record and group overlap across splits", () => {
+ const packet = samples.find((item) => item.id === "split-hold-002");
+ assert.equal(pairwiseOverlaps(packet, "recordIds").length, 1);
+ assert.equal(pairwiseOverlaps(packet, "groupIds").length, 1);
+});
+
+test("detects stratification balance drift", () => {
+ const drift = classBalanceDrift(samples.find((item) => item.id === "split-review-003"));
+ assert.ok(drift.length >= 2);
+});
+
+test("releases a clean split package", () => {
+ const result = analyzePacket(samples.find((item) => item.id === "split-release-001"));
+ assert.equal(result.decision, "RELEASE");
+ assert.equal(result.findings.length, 0);
+});
+
+test("holds split packages with leakage and embargoed rows", () => {
+ const result = analyzePacket(samples.find((item) => item.id === "split-hold-002"));
+ assert.equal(result.decision, "HOLD_RELEASE");
+ assert.ok(result.findings.some((item) => item.code === "TRAIN_VALIDATION_TEST_RECORD_OVERLAP"));
+ assert.ok(result.findings.some((item) => item.code === "GROUP_LEAKAGE_ACROSS_SPLITS"));
+ assert.ok(result.findings.some((item) => item.code === "EMBARGOED_RECORD_IN_PUBLIC_SPLIT"));
+ assert.ok(result.findings.some((item) => item.code === "PUBLIC_RELEASE_NOT_FROZEN_FOR_SPLIT_RISK"));
+});
+
+test("routes provenance and balance issues for review", () => {
+ const result = analyzePacket(samples.find((item) => item.id === "split-review-003"));
+ assert.equal(result.decision, "REVIEW_RELEASE");
+ assert.ok(result.findings.some((item) => item.code === "SPLIT_PROVENANCE_INCOMPLETE"));
+ assert.ok(result.findings.some((item) => item.code === "STRATIFICATION_BALANCE_DRIFT"));
+});
+
+test("holds missing test split before public release", () => {
+ const result = analyzePacket(samples.find((item) => item.id === "split-hold-004"));
+ assert.equal(result.decision, "HOLD_RELEASE");
+ assert.ok(result.findings.some((item) => item.code === "REQUIRED_SPLIT_MISSING"));
+});
+
+test("aggregates split release decisions", () => {
+ const report = analyzeSplitReleasePackets(samples);
+ assert.equal(report.totalPackets, 4);
+ assert.deepEqual(report.counts, { RELEASE: 1, REVIEW_RELEASE: 1, HOLD_RELEASE: 2 });
+});