diff --git a/challenge-evidence-freeze-gate/README.md b/challenge-evidence-freeze-gate/README.md
new file mode 100644
index 0000000..4cc21b2
--- /dev/null
+++ b/challenge-evidence-freeze-gate/README.md
@@ -0,0 +1,30 @@
+# Challenge Evidence Freeze Gate
+
+This module is a focused slice for SCIBASE.AI issue #18, Scientific Bounty System.
+
+It freezes solver submission evidence at the challenge deadline, compares later sponsor/reviewer packets against the frozen manifest, and emits tamper-diff decisions before scoring, arbitration, payout, or IP transfer.
+
+## What It Covers
+
+- Deadline-locked artifact manifests and manifest hashes.
+- Post-deadline checksum drift, metric drift, metadata drift, removals, and late-added artifacts.
+- Reviewer-visible exception justifications.
+- Scoring exclusions and payout/arbitration hold decisions.
+- Public exception notes and reviewer task packets.
+- JSON audit packets, Markdown arbitration packets, SVG summaries, and a short MP4 demo artifact.
+
+The sample data is synthetic. The module is dependency-free and does not call external services.
+
+## Run
+
+```bash
+node challenge-evidence-freeze-gate/test.js
+node challenge-evidence-freeze-gate/demo.js
+```
+
+Demo output is written to:
+
+- `reports/freeze-audit.json`
+- `reports/arbitration-packet.md`
+- `reports/freeze-gate.svg`
+- `reports/demo.mp4`
diff --git a/challenge-evidence-freeze-gate/demo.js b/challenge-evidence-freeze-gate/demo.js
new file mode 100644
index 0000000..fb293af
--- /dev/null
+++ b/challenge-evidence-freeze-gate/demo.js
@@ -0,0 +1,25 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const data = require("./sample-data");
+const { renderMarkdown, renderSvg, runFreezeGate } = require("./index");
+
+const report = runFreezeGate(data);
+const reportsDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+fs.writeFileSync(path.join(reportsDir, "freeze-audit.json"), JSON.stringify(report, null, 2));
+fs.writeFileSync(path.join(reportsDir, "arbitration-packet.md"), renderMarkdown(report));
+fs.writeFileSync(path.join(reportsDir, "freeze-gate.svg"), renderSvg(report));
+
+console.log(JSON.stringify({
+ decision: report.summary.releaseDecision,
+ blockerCount: report.summary.blockerCount,
+ totalDiffs: report.summary.totalDiffs,
+ excludedFromScoring: [...new Set(report.arbitrationPacket.excludedFromScoring)],
+ auditDigest: report.auditDigest,
+ reports: [
+ "reports/freeze-audit.json",
+ "reports/arbitration-packet.md",
+ "reports/freeze-gate.svg",
+ "reports/demo.mp4"
+ ]
+}, null, 2));
diff --git a/challenge-evidence-freeze-gate/index.js b/challenge-evidence-freeze-gate/index.js
new file mode 100644
index 0000000..4d68588
--- /dev/null
+++ b/challenge-evidence-freeze-gate/index.js
@@ -0,0 +1,268 @@
+const crypto = require("node:crypto");
+
+const BLOCKING_DIFFS = new Set([
+ "primary-artifact-added-after-deadline",
+ "checksum-drift",
+ "metric-drift"
+]);
+
+function stableStringify(value) {
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
+ if (value && typeof value === "object") {
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
+ }
+ return JSON.stringify(value);
+}
+
+function digest(value) {
+ return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
+}
+
+function byId(items) {
+ return new Map(items.map((item) => [item.id, item]));
+}
+
+function isPrimary(artifact) {
+ return ["primary-model", "primary-result", "dataset", "analysis-notebook"].includes(artifact.type);
+}
+
+function addDiff(diffs, severity, code, artifactId, message, task, justification = "") {
+ diffs.push({ severity, code, artifactId, message, task, justification });
+}
+
+function compareArtifact(frozen, current, challenge) {
+ const diffs = [];
+ if (frozen.checksum !== current.checksum) {
+ addDiff(
+ diffs,
+ challenge.rules.blockChecksumDrift ? "blocker" : "high",
+ "checksum-drift",
+ current.id,
+ `${current.id} checksum changed from ${frozen.checksum} to ${current.checksum}.`,
+ "Freeze evaluation for this artifact and route to arbitration before scoring.",
+ current.exceptionJustification || ""
+ );
+ }
+
+ if (frozen.metric?.name === current.metric?.name && frozen.metric?.value !== current.metric?.value) {
+ addDiff(
+ diffs,
+ challenge.rules.blockMetricDrift ? "blocker" : "high",
+ "metric-drift",
+ current.id,
+ `${current.id} metric ${current.metric.name} changed from ${frozen.metric.value} to ${current.metric.value}.`,
+ "Require reviewer-visible metric-change explanation and pre-deadline reproduction evidence.",
+ current.exceptionJustification || ""
+ );
+ }
+
+ if (frozen.bytes !== current.bytes && frozen.checksum === current.checksum) {
+ addDiff(
+ diffs,
+ "medium",
+ "metadata-size-drift",
+ current.id,
+ `${current.id} size changed while checksum stayed fixed.`,
+ "Verify packaging metadata and storage manifest generation.",
+ current.exceptionJustification || ""
+ );
+ }
+
+ if (JSON.stringify(frozen.metadata) !== JSON.stringify(current.metadata)) {
+ addDiff(
+ diffs,
+ "medium",
+ "metadata-drift",
+ current.id,
+ `${current.id} metadata changed after freeze.`,
+ "Record a reviewer-visible metadata exception before releasing the packet.",
+ current.exceptionJustification || ""
+ );
+ }
+
+ if (diffs.length && challenge.rules.requireReviewerVisibleJustification && !current.exceptionJustification) {
+ addDiff(
+ diffs,
+ "high",
+ "exception-justification-missing",
+ current.id,
+ `${current.id} changed after freeze without a reviewer-visible justification.`,
+ "Block sponsor preview until the solver provides an exception justification."
+ );
+ }
+
+ return diffs;
+}
+
+function compareSubmissions(challenge, frozenSubmission, currentSubmission) {
+ const frozenMap = byId(frozenSubmission.artifacts);
+ const currentMap = byId(currentSubmission.artifacts);
+ const diffs = [];
+
+ for (const current of currentSubmission.artifacts) {
+ const frozen = frozenMap.get(current.id);
+ if (!frozen) {
+ const primary = isPrimary(current);
+ addDiff(
+ diffs,
+ primary && challenge.rules.blockLatePrimaryArtifacts ? "blocker" : "high",
+ primary ? "primary-artifact-added-after-deadline" : "artifact-added-after-deadline",
+ current.id,
+ `${current.id} was added after the frozen deadline manifest.`,
+ primary ? "Exclude this artifact from scoring and hold payout/arbitration until reviewed." : "Route late artifact to reviewer exception queue.",
+ current.exceptionJustification || ""
+ );
+ if (challenge.rules.requireReviewerVisibleJustification && !current.exceptionJustification) {
+ addDiff(
+ diffs,
+ "high",
+ "exception-justification-missing",
+ current.id,
+ `${current.id} has no reviewer-visible late-addition justification.`,
+ "Require a public exception note before sponsor review."
+ );
+ }
+ continue;
+ }
+ diffs.push(...compareArtifact(frozen, current, challenge));
+ }
+
+ for (const frozen of frozenSubmission.artifacts) {
+ if (!currentMap.has(frozen.id)) {
+ addDiff(
+ diffs,
+ "blocker",
+ "artifact-removed-after-deadline",
+ frozen.id,
+ `${frozen.id} was present in the frozen manifest but is absent from the current packet.`,
+ "Restore the frozen artifact or mark the submission incomplete before evaluation."
+ );
+ }
+ }
+
+ return diffs;
+}
+
+function summarizeDiffs(diffs) {
+ const countsBySeverity = diffs.reduce((counts, diff) => {
+ counts[diff.severity] = (counts[diff.severity] || 0) + 1;
+ return counts;
+ }, {});
+ const countsByCode = diffs.reduce((counts, diff) => {
+ counts[diff.code] = (counts[diff.code] || 0) + 1;
+ return counts;
+ }, {});
+ const blockerCount = diffs.filter((diff) => diff.severity === "blocker").length;
+ const releaseDecision = blockerCount
+ ? "hold-evaluation-and-payout"
+ : diffs.length
+ ? "review-exceptions-before-scoring"
+ : "frozen-packet-clean";
+ return {
+ releaseDecision,
+ blockerCount,
+ totalDiffs: diffs.length,
+ countsBySeverity,
+ countsByCode
+ };
+}
+
+function buildArbitrationPacket(challenge, frozenSubmission, currentSubmission, diffs) {
+ return {
+ challengeId: challenge.id,
+ submissionId: frozenSubmission.submissionId,
+ frozenAt: frozenSubmission.frozenAt,
+ observedAt: currentSubmission.observedAt,
+ frozenManifestHash: frozenSubmission.manifestHash,
+ currentManifestHash: currentSubmission.manifestHash,
+ excludedFromScoring: diffs
+ .filter((diff) => diff.severity === "blocker" || BLOCKING_DIFFS.has(diff.code))
+ .map((diff) => diff.artifactId),
+ reviewerTasks: diffs.map((diff) => diff.task),
+ publicExceptionNotes: diffs
+ .filter((diff) => diff.justification)
+ .map((diff) => ({ artifactId: diff.artifactId, justification: diff.justification }))
+ };
+}
+
+function runFreezeGate(data) {
+ const diffs = compareSubmissions(data.challenge, data.frozenSubmission, data.currentSubmission);
+ const summary = summarizeDiffs(diffs);
+ const arbitrationPacket = buildArbitrationPacket(data.challenge, data.frozenSubmission, data.currentSubmission, diffs);
+ const packet = {
+ module: "challenge-evidence-freeze-gate",
+ challenge: {
+ id: data.challenge.id,
+ title: data.challenge.title,
+ deadline: data.challenge.deadline,
+ reviewOpenedAt: data.challenge.reviewOpenedAt
+ },
+ submission: {
+ id: data.frozenSubmission.submissionId,
+ teamId: data.frozenSubmission.teamId
+ },
+ summary,
+ diffs,
+ arbitrationPacket
+ };
+ return {
+ ...packet,
+ auditDigest: digest(packet)
+ };
+}
+
+function renderMarkdown(report) {
+ const lines = [
+ `# Challenge Evidence Freeze Review: ${report.challenge.title}`,
+ "",
+ `Decision: **${report.summary.releaseDecision}**`,
+ `Blockers: **${report.summary.blockerCount}**`,
+ `Total diffs: **${report.summary.totalDiffs}**`,
+ "",
+ "## Tamper Diff Queue"
+ ];
+ for (const diff of report.diffs) {
+ lines.push(`- [${diff.severity}] ${diff.artifactId}: ${diff.message}`);
+ lines.push(` Task: ${diff.task}`);
+ if (diff.justification) lines.push(` Justification: ${diff.justification}`);
+ }
+ lines.push("", "## Arbitration Packet");
+ lines.push(`- Frozen manifest: ${report.arbitrationPacket.frozenManifestHash}`);
+ lines.push(`- Current manifest: ${report.arbitrationPacket.currentManifestHash}`);
+ lines.push(`- Excluded from scoring: ${[...new Set(report.arbitrationPacket.excludedFromScoring)].join(", ") || "none"}`);
+ lines.push("", `Audit digest: \`${report.auditDigest}\``);
+ return `${lines.join("\n")}\n`;
+}
+
+function renderSvg(report) {
+ const width = 980;
+ const height = 500;
+ const blocker = report.summary.blockerCount;
+ const high = report.summary.countsBySeverity.high || 0;
+ const medium = report.summary.countsBySeverity.medium || 0;
+ return [
+ ``
+ ].join("");
+}
+
+module.exports = {
+ runFreezeGate,
+ compareSubmissions,
+ renderMarkdown,
+ renderSvg,
+ digest
+};
diff --git a/challenge-evidence-freeze-gate/reports/arbitration-packet.md b/challenge-evidence-freeze-gate/reports/arbitration-packet.md
new file mode 100644
index 0000000..1c0fee1
--- /dev/null
+++ b/challenge-evidence-freeze-gate/reports/arbitration-packet.md
@@ -0,0 +1,30 @@
+# Challenge Evidence Freeze Review: Single-cell biomarker forecast challenge
+
+Decision: **hold-evaluation-and-payout**
+Blockers: **4**
+Total diffs: **6**
+
+## Tamper Diff Queue
+- [blocker] model: model checksum changed from sha256:model-v1 to sha256:model-v2.
+ Task: Freeze evaluation for this artifact and route to arbitration before scoring.
+ Justification: retrained after discovering seed typo
+- [blocker] model: model metric heldout_auc changed from 0.842 to 0.861.
+ Task: Require reviewer-visible metric-change explanation and pre-deadline reproduction evidence.
+ Justification: retrained after discovering seed typo
+- [blocker] whitepaper: whitepaper metric page_count changed from 12 to 13.
+ Task: Require reviewer-visible metric-change explanation and pre-deadline reproduction evidence.
+ Justification: added funding acknowledgement only
+- [medium] whitepaper: whitepaper size changed while checksum stayed fixed.
+ Task: Verify packaging metadata and storage manifest generation.
+ Justification: added funding acknowledgement only
+- [blocker] supplement: supplement was added after the frozen deadline manifest.
+ Task: Exclude this artifact from scoring and hold payout/arbitration until reviewed.
+- [high] supplement: supplement has no reviewer-visible late-addition justification.
+ Task: Require a public exception note before sponsor review.
+
+## Arbitration Packet
+- Frozen manifest: sha256:frozen-manifest-001
+- Current manifest: sha256:current-manifest-002
+- Excluded from scoring: model, whitepaper, supplement
+
+Audit digest: `f9aacb2b51e4f942d760548b7e4ebcd13705da0243151e2a223c56e2d8333901`
diff --git a/challenge-evidence-freeze-gate/reports/demo.mp4 b/challenge-evidence-freeze-gate/reports/demo.mp4
new file mode 100644
index 0000000..8a93ea8
Binary files /dev/null and b/challenge-evidence-freeze-gate/reports/demo.mp4 differ
diff --git a/challenge-evidence-freeze-gate/reports/freeze-audit.json b/challenge-evidence-freeze-gate/reports/freeze-audit.json
new file mode 100644
index 0000000..d5f0545
--- /dev/null
+++ b/challenge-evidence-freeze-gate/reports/freeze-audit.json
@@ -0,0 +1,121 @@
+{
+ "module": "challenge-evidence-freeze-gate",
+ "challenge": {
+ "id": "challenge-cell-atlas-forecast",
+ "title": "Single-cell biomarker forecast challenge",
+ "deadline": "2026-05-18T23:59:59Z",
+ "reviewOpenedAt": "2026-05-19T09:00:00Z"
+ },
+ "submission": {
+ "id": "team-northlake-final",
+ "teamId": "team-northlake"
+ },
+ "summary": {
+ "releaseDecision": "hold-evaluation-and-payout",
+ "blockerCount": 4,
+ "totalDiffs": 6,
+ "countsBySeverity": {
+ "blocker": 4,
+ "medium": 1,
+ "high": 1
+ },
+ "countsByCode": {
+ "checksum-drift": 1,
+ "metric-drift": 2,
+ "metadata-size-drift": 1,
+ "primary-artifact-added-after-deadline": 1,
+ "exception-justification-missing": 1
+ }
+ },
+ "diffs": [
+ {
+ "severity": "blocker",
+ "code": "checksum-drift",
+ "artifactId": "model",
+ "message": "model checksum changed from sha256:model-v1 to sha256:model-v2.",
+ "task": "Freeze evaluation for this artifact and route to arbitration before scoring.",
+ "justification": "retrained after discovering seed typo"
+ },
+ {
+ "severity": "blocker",
+ "code": "metric-drift",
+ "artifactId": "model",
+ "message": "model metric heldout_auc changed from 0.842 to 0.861.",
+ "task": "Require reviewer-visible metric-change explanation and pre-deadline reproduction evidence.",
+ "justification": "retrained after discovering seed typo"
+ },
+ {
+ "severity": "blocker",
+ "code": "metric-drift",
+ "artifactId": "whitepaper",
+ "message": "whitepaper metric page_count changed from 12 to 13.",
+ "task": "Require reviewer-visible metric-change explanation and pre-deadline reproduction evidence.",
+ "justification": "added funding acknowledgement only"
+ },
+ {
+ "severity": "medium",
+ "code": "metadata-size-drift",
+ "artifactId": "whitepaper",
+ "message": "whitepaper size changed while checksum stayed fixed.",
+ "task": "Verify packaging metadata and storage manifest generation.",
+ "justification": "added funding acknowledgement only"
+ },
+ {
+ "severity": "blocker",
+ "code": "primary-artifact-added-after-deadline",
+ "artifactId": "supplement",
+ "message": "supplement was added after the frozen deadline manifest.",
+ "task": "Exclude this artifact from scoring and hold payout/arbitration until reviewed.",
+ "justification": ""
+ },
+ {
+ "severity": "high",
+ "code": "exception-justification-missing",
+ "artifactId": "supplement",
+ "message": "supplement has no reviewer-visible late-addition justification.",
+ "task": "Require a public exception note before sponsor review.",
+ "justification": ""
+ }
+ ],
+ "arbitrationPacket": {
+ "challengeId": "challenge-cell-atlas-forecast",
+ "submissionId": "team-northlake-final",
+ "frozenAt": "2026-05-18T23:58:44Z",
+ "observedAt": "2026-05-19T14:12:00Z",
+ "frozenManifestHash": "sha256:frozen-manifest-001",
+ "currentManifestHash": "sha256:current-manifest-002",
+ "excludedFromScoring": [
+ "model",
+ "model",
+ "whitepaper",
+ "supplement"
+ ],
+ "reviewerTasks": [
+ "Freeze evaluation for this artifact and route to arbitration before scoring.",
+ "Require reviewer-visible metric-change explanation and pre-deadline reproduction evidence.",
+ "Require reviewer-visible metric-change explanation and pre-deadline reproduction evidence.",
+ "Verify packaging metadata and storage manifest generation.",
+ "Exclude this artifact from scoring and hold payout/arbitration until reviewed.",
+ "Require a public exception note before sponsor review."
+ ],
+ "publicExceptionNotes": [
+ {
+ "artifactId": "model",
+ "justification": "retrained after discovering seed typo"
+ },
+ {
+ "artifactId": "model",
+ "justification": "retrained after discovering seed typo"
+ },
+ {
+ "artifactId": "whitepaper",
+ "justification": "added funding acknowledgement only"
+ },
+ {
+ "artifactId": "whitepaper",
+ "justification": "added funding acknowledgement only"
+ }
+ ]
+ },
+ "auditDigest": "f9aacb2b51e4f942d760548b7e4ebcd13705da0243151e2a223c56e2d8333901"
+}
\ No newline at end of file
diff --git a/challenge-evidence-freeze-gate/reports/freeze-gate.svg b/challenge-evidence-freeze-gate/reports/freeze-gate.svg
new file mode 100644
index 0000000..8776724
--- /dev/null
+++ b/challenge-evidence-freeze-gate/reports/freeze-gate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/challenge-evidence-freeze-gate/requirements-map.md b/challenge-evidence-freeze-gate/requirements-map.md
new file mode 100644
index 0000000..8ab7f8c
--- /dev/null
+++ b/challenge-evidence-freeze-gate/requirements-map.md
@@ -0,0 +1,17 @@
+# Requirements Map
+
+Source: SCIBASE.AI issue #18, Scientific Bounty System.
+
+| Requirement | Implementation |
+| --- | --- |
+| Submission engine | Frozen and current solver submission packets are modeled with artifact manifests. |
+| Version control and audit logs | `manifestHash`, artifact checksums, timestamps, and deterministic audit digests record submission history. |
+| Built-in submission package builder | Artifact manifests include model, notebook, whitepaper, and result package metadata. |
+| Multi-phase challenge support | The gate sits between deadline close and review/arbitration scoring. |
+| Automated deliverable checklists | `compareSubmissions()` checks late additions, removals, checksum drift, metric drift, and metadata drift. |
+| Arbitration workflow | `arbitrationPacket` emits exclusions, reviewer tasks, public exception notes, and hold decisions. |
+| Escrowed prize and payout readiness | Blocking diffs produce `hold-evaluation-and-payout` before reward or settlement. |
+| IP transfer guard | Artifacts retain solver-owned licensing until clean evaluation and payout readiness. |
+| Sponsor/reviewer transparency | Exception justifications are captured as reviewer-visible notes. |
+| Reviewer-facing artifacts | Demo emits JSON, Markdown, SVG, and MP4 artifacts under `reports/`. |
+| Local verification | `test.js` covers checksum drift, metric drift, late primary artifacts, missing justifications, clean packet behavior, digest stability, Markdown output, and SVG output. |
diff --git a/challenge-evidence-freeze-gate/sample-data.js b/challenge-evidence-freeze-gate/sample-data.js
new file mode 100644
index 0000000..d28e1e3
--- /dev/null
+++ b/challenge-evidence-freeze-gate/sample-data.js
@@ -0,0 +1,104 @@
+const challenge = {
+ id: "challenge-cell-atlas-forecast",
+ title: "Single-cell biomarker forecast challenge",
+ deadline: "2026-05-18T23:59:59Z",
+ reviewOpenedAt: "2026-05-19T09:00:00Z",
+ prizeAmountUsd: 100000,
+ rules: {
+ allowPostDeadlineMetadataFixes: true,
+ requireReviewerVisibleJustification: true,
+ blockLatePrimaryArtifacts: true,
+ blockChecksumDrift: true,
+ blockMetricDrift: true
+ }
+};
+
+const frozenSubmission = {
+ submissionId: "team-northlake-final",
+ teamId: "team-northlake",
+ frozenAt: "2026-05-18T23:58:44Z",
+ manifestHash: "sha256:frozen-manifest-001",
+ artifacts: [
+ {
+ id: "model",
+ path: "models/biomarker-forecast.pkl",
+ type: "primary-model",
+ checksum: "sha256:model-v1",
+ bytes: 2481000,
+ metric: { name: "heldout_auc", value: 0.842 },
+ metadata: { license: "solver-retained-until-paid", reviewerVisible: true }
+ },
+ {
+ id: "notebook",
+ path: "analysis/final-evaluation.ipynb",
+ type: "analysis-notebook",
+ checksum: "sha256:notebook-v1",
+ bytes: 811000,
+ metric: { name: "rerun_status", value: "passed" },
+ metadata: { kernel: "python3.12", reviewerVisible: true }
+ },
+ {
+ id: "whitepaper",
+ path: "docs/methods-whitepaper.pdf",
+ type: "whitepaper",
+ checksum: "sha256:whitepaper-v1",
+ bytes: 402200,
+ metric: { name: "page_count", value: 12 },
+ metadata: { license: "solver-retained-until-paid", reviewerVisible: true }
+ }
+ ]
+};
+
+const currentSubmission = {
+ submissionId: "team-northlake-final",
+ teamId: "team-northlake",
+ observedAt: "2026-05-19T14:12:00Z",
+ manifestHash: "sha256:current-manifest-002",
+ artifacts: [
+ {
+ id: "model",
+ path: "models/biomarker-forecast.pkl",
+ type: "primary-model",
+ checksum: "sha256:model-v2",
+ bytes: 2481400,
+ metric: { name: "heldout_auc", value: 0.861 },
+ metadata: { license: "solver-retained-until-paid", reviewerVisible: true },
+ exceptionJustification: "retrained after discovering seed typo"
+ },
+ {
+ id: "notebook",
+ path: "analysis/final-evaluation.ipynb",
+ type: "analysis-notebook",
+ checksum: "sha256:notebook-v1",
+ bytes: 811000,
+ metric: { name: "rerun_status", value: "passed" },
+ metadata: { kernel: "python3.12", reviewerVisible: true }
+ },
+ {
+ id: "whitepaper",
+ path: "docs/methods-whitepaper.pdf",
+ type: "whitepaper",
+ checksum: "sha256:whitepaper-v1",
+ bytes: 405900,
+ metric: { name: "page_count", value: 13 },
+ metadata: { license: "solver-retained-until-paid", reviewerVisible: true },
+ exceptionJustification: "added funding acknowledgement only"
+ },
+ {
+ id: "supplement",
+ path: "supplement/post-deadline-ablation.csv",
+ type: "primary-result",
+ checksum: "sha256:late-ablation",
+ bytes: 78000,
+ metric: { name: "ablation_gain", value: 0.04 },
+ metadata: { license: "solver-retained-until-paid", reviewerVisible: true },
+ exceptionJustification: ""
+ }
+ ]
+};
+
+module.exports = {
+ challenge,
+ frozenSubmission,
+ currentSubmission
+};
diff --git a/challenge-evidence-freeze-gate/test.js b/challenge-evidence-freeze-gate/test.js
new file mode 100644
index 0000000..3c44072
--- /dev/null
+++ b/challenge-evidence-freeze-gate/test.js
@@ -0,0 +1,33 @@
+const assert = require("node:assert/strict");
+const data = require("./sample-data");
+const { compareSubmissions, digest, renderMarkdown, renderSvg, runFreezeGate } = require("./index");
+
+const report = runFreezeGate(data);
+
+assert.equal(report.module, "challenge-evidence-freeze-gate");
+assert.equal(report.summary.releaseDecision, "hold-evaluation-and-payout");
+assert.equal(report.summary.blockerCount, 4);
+assert.ok(report.auditDigest.match(/^[a-f0-9]{64}$/));
+
+assert.ok(report.diffs.some((diff) => diff.code === "checksum-drift" && diff.artifactId === "model"));
+assert.ok(report.diffs.some((diff) => diff.code === "metric-drift" && diff.artifactId === "model"));
+assert.ok(report.diffs.some((diff) => diff.code === "primary-artifact-added-after-deadline" && diff.artifactId === "supplement"));
+assert.ok(report.diffs.some((diff) => diff.code === "exception-justification-missing" && diff.artifactId === "supplement"));
+assert.ok(report.diffs.some((diff) => diff.code === "metadata-size-drift" && diff.artifactId === "whitepaper"));
+
+assert.ok(report.arbitrationPacket.excludedFromScoring.includes("model"));
+assert.ok(report.arbitrationPacket.excludedFromScoring.includes("supplement"));
+assert.ok(report.arbitrationPacket.publicExceptionNotes.some((note) => note.artifactId === "model"));
+
+const cleanCurrent = {
+ ...data.currentSubmission,
+ manifestHash: data.frozenSubmission.manifestHash,
+ artifacts: data.frozenSubmission.artifacts
+};
+assert.deepEqual(compareSubmissions(data.challenge, data.frozenSubmission, cleanCurrent), []);
+
+assert.equal(digest({ b: 2, a: 1 }), digest({ a: 1, b: 2 }));
+assert.ok(renderMarkdown(report).includes("Challenge Evidence Freeze Review"));
+assert.ok(renderSvg(report).startsWith("