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 [ + ``, + ``, + ``, + `Challenge Evidence Freeze Gate`, + `${report.summary.releaseDecision} | Audit ${report.auditDigest.slice(0, 12)}`, + ``, + `Blockers: ${blocker}`, + ``, + `High: ${high}`, + ``, + `Medium: ${medium}`, + `Excluded: ${[...new Set(report.arbitrationPacket.excludedFromScoring)].join(", ")}`, + `Frozen ${report.arbitrationPacket.frozenManifestHash}`, + `Current ${report.arbitrationPacket.currentManifestHash}`, + `` + ].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 @@ +Challenge Evidence Freeze Gatehold-evaluation-and-payout | Audit f9aacb2b51e4Blockers: 4High: 1Medium: 1Excluded: model, whitepaper, supplementFrozen sha256:frozen-manifest-001Current sha256:current-manifest-002 \ 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("