diff --git a/scientific-bounty-system/anonymous-review-packet-guard/README.md b/scientific-bounty-system/anonymous-review-packet-guard/README.md
new file mode 100644
index 00000000..593892db
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/README.md
@@ -0,0 +1,36 @@
+# Anonymous Review Packet Guard
+
+This module is a focused Scientific Bounty System slice for issue #18. It validates anonymous challenge review packets before sponsor or reviewer access so solver identity does not leak through metadata, files, notebooks, commits, or access grants.
+
+It checks:
+
+- commit author names and emails
+- local file paths, lab folders, and institution references
+- notebook outputs containing usernames, paths, or emails
+- document author/company/editor metadata
+- EXIF GPS, device-owner, and serial metadata
+- acknowledgements and team-bio sections
+- sponsor/reviewer grants to unredacted packets
+- missing redaction receipts
+- audit logs that expose identity outside arbitrators
+
+All fixtures are synthetic. The module does not use credentials, private submissions, live challenge data, external APIs, payment rails, or platform secrets.
+
+## Run
+
+```sh
+node scientific-bounty-system/anonymous-review-packet-guard/test.js
+node scientific-bounty-system/anonymous-review-packet-guard/demo.js
+node scientific-bounty-system/anonymous-review-packet-guard/make-demo-video.js
+```
+
+The demo writes reviewer artifacts to `reports/`:
+
+- `anonymous-review-packet-audit.json`
+- `anonymous-review-packet-audit.md`
+- `anonymous-review-packet-summary.svg`
+- `demo.mp4`
+
+## Bounty Fit
+
+Issue #18 calls for anonymous or named participation, secure submission workspaces, standardized submission packages, sponsor review, arbitration, and IP-safe release. This guard implements the identity-redaction gate that has to pass before anonymous submissions can be reviewed without compromising solver anonymity or arbitration evidence.
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/demo.js b/scientific-bounty-system/anonymous-review-packet-guard/demo.js
new file mode 100644
index 00000000..d3b56daf
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/demo.js
@@ -0,0 +1,14 @@
+const path = require("path");
+const { auditAnonymousReviewPacket, writeAuditReports } = require("./index");
+const { samplePacket } = require("./sample-data");
+
+const report = auditAnonymousReviewPacket(samplePacket);
+const reportsDir = path.join(__dirname, "reports");
+const outputs = writeAuditReports(report, reportsDir);
+
+console.log(`decision=${report.decision} riskScore=${report.riskScore} findings=${report.findingCount}`);
+console.log(`digest=${report.evidenceDigest}`);
+console.log(`reports=${reportsDir}`);
+console.log(`json=${outputs.jsonPath}`);
+console.log(`markdown=${outputs.markdownPath}`);
+console.log(`svg=${outputs.svgPath}`);
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/index.js b/scientific-bounty-system/anonymous-review-packet-guard/index.js
new file mode 100644
index 00000000..988f2a64
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/index.js
@@ -0,0 +1,396 @@
+const crypto = require("crypto");
+const fs = require("fs");
+const path = require("path");
+
+const RULES = {
+ COMMIT_AUTHOR_LEAK: {
+ severity: "high",
+ penalty: 18,
+ recommendation: "Rewrite or mask commit author metadata before the sponsor review packet is released.",
+ },
+ FILE_PATH_IDENTITY_LEAK: {
+ severity: "high",
+ penalty: 16,
+ recommendation: "Replace local usernames, lab folders, and institution paths with neutral artifact paths.",
+ },
+ NOTEBOOK_OUTPUT_IDENTITY_LEAK: {
+ severity: "high",
+ penalty: 15,
+ recommendation: "Clear notebook outputs or redact user, host, and local path fragments before reviewer access.",
+ },
+ DOCUMENT_METADATA_LEAK: {
+ severity: "high",
+ penalty: 15,
+ recommendation: "Strip document author, company, editor, and template metadata from review exports.",
+ },
+ EXIF_GPS_OR_DEVICE_LEAK: {
+ severity: "medium",
+ penalty: 10,
+ recommendation: "Remove EXIF GPS, serial, device owner, and capture metadata from figures and media.",
+ },
+ ACKNOWLEDGEMENT_IDENTITY_LEAK: {
+ severity: "medium",
+ penalty: 9,
+ recommendation: "Move acknowledgements and institution references to the post-award disclosure packet.",
+ },
+ UNBLINDED_REVIEWER_ROLE: {
+ severity: "high",
+ penalty: 17,
+ recommendation: "Remove sponsor/reviewer access until all anonymous review roles use the redacted packet.",
+ },
+ REDACTION_EVIDENCE_MISSING: {
+ severity: "medium",
+ penalty: 8,
+ recommendation: "Attach redaction receipts for every transformed artifact before review release.",
+ },
+ ANONYMITY_MODE_MISMATCH: {
+ severity: "high",
+ penalty: 18,
+ recommendation: "Hold the submission until the challenge anonymity setting matches the generated packet.",
+ },
+ AUDIT_LOG_UNPROTECTED: {
+ severity: "medium",
+ penalty: 7,
+ recommendation: "Keep full identity audit logs restricted to arbitrators until award or dispute resolution.",
+ },
+};
+
+const IDENTITY_PATTERNS = [
+ { id: "email", pattern: /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i },
+ { id: "home-directory", pattern: /\/Users\/[A-Za-z0-9._-]+|\/home\/[A-Za-z0-9._-]+/i },
+ { id: "windows-profile", pattern: /C:\\Users\\[A-Za-z0-9._-]+/i },
+ { id: "institution", pattern: /\b(Stanford|MIT|Harvard|Oxford|Cambridge|Berkeley|Caltech|UCLA|University|Institute)\b/i },
+ { id: "orcid", pattern: /\b\d{4}-\d{4}-\d{4}-\d{3}[\dX]\b/i },
+ { id: "github-handle", pattern: /@[A-Za-z0-9-]{3,39}\b/ },
+];
+
+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, length = 16) {
+ return crypto.createHash("sha256").update(stableStringify(value)).digest("hex").slice(0, length);
+}
+
+function addFinding(findings, code, message, evidence) {
+ const rule = RULES[code];
+ findings.push({
+ code,
+ severity: rule.severity,
+ penalty: rule.penalty,
+ message,
+ evidence,
+ recommendation: rule.recommendation,
+ });
+}
+
+function detectIdentity(text) {
+ const value = String(text || "");
+ return IDENTITY_PATTERNS.filter((entry) => entry.pattern.test(value)).map((entry) => entry.id);
+}
+
+function auditChallengeMode(packet, findings) {
+ if (packet.challenge.anonymityMode !== packet.reviewPacket.anonymityMode) {
+ addFinding(
+ findings,
+ "ANONYMITY_MODE_MISMATCH",
+ "Challenge anonymity mode does not match the generated review packet.",
+ {
+ challengeMode: packet.challenge.anonymityMode,
+ reviewPacketMode: packet.reviewPacket.anonymityMode,
+ },
+ );
+ }
+}
+
+function auditArtifacts(packet, findings) {
+ for (const artifact of packet.artifacts || []) {
+ const pathHits = detectIdentity(artifact.path);
+ if (pathHits.length > 0) {
+ addFinding(findings, "FILE_PATH_IDENTITY_LEAK", `Artifact ${artifact.id} path contains identity-bearing text.`, {
+ artifactId: artifact.id,
+ path: artifact.path,
+ detectedSignals: pathHits,
+ });
+ }
+
+ const metadataText = JSON.stringify(artifact.metadata || {});
+ const metadataHits = detectIdentity(metadataText);
+ if (metadataHits.length > 0 || artifact.metadata.author || artifact.metadata.company || artifact.metadata.lastModifiedBy) {
+ addFinding(findings, "DOCUMENT_METADATA_LEAK", `Artifact ${artifact.id} metadata can identify the solver team.`, {
+ artifactId: artifact.id,
+ metadata: artifact.metadata,
+ detectedSignals: metadataHits,
+ });
+ }
+
+ if (artifact.type === "notebook") {
+ const outputHits = (artifact.outputs || [])
+ .map((output, index) => ({ index, signals: detectIdentity(output), value: output }))
+ .filter((entry) => entry.signals.length > 0);
+ if (outputHits.length > 0) {
+ addFinding(findings, "NOTEBOOK_OUTPUT_IDENTITY_LEAK", `Notebook ${artifact.id} output contains solver identity signals.`, {
+ artifactId: artifact.id,
+ outputHits,
+ });
+ }
+ }
+
+ if (artifact.type === "image" || artifact.type === "media") {
+ const exif = artifact.metadata.exif || {};
+ if (exif.gps || exif.deviceOwner || exif.serialNumber) {
+ addFinding(findings, "EXIF_GPS_OR_DEVICE_LEAK", `Media artifact ${artifact.id} contains identifying EXIF metadata.`, {
+ artifactId: artifact.id,
+ exif,
+ });
+ }
+ }
+ }
+}
+
+function auditCommits(packet, findings) {
+ for (const commit of packet.commitHistory || []) {
+ const signals = [
+ ...detectIdentity(commit.authorName),
+ ...detectIdentity(commit.authorEmail),
+ ...detectIdentity(commit.message),
+ ];
+ const redactedAuthor = /^(anon|anonymous|redacted)([-_\s]solver)?$/i.test(String(commit.authorName || ""));
+ const hasUnredactedAuthorName = Boolean(commit.authorName) && !redactedAuthor;
+ if (signals.length > 0 || commit.authorEmail || hasUnredactedAuthorName) {
+ addFinding(findings, "COMMIT_AUTHOR_LEAK", `Commit ${commit.sha} exposes solver identity metadata.`, {
+ sha: commit.sha,
+ authorName: commit.authorName,
+ authorEmail: commit.authorEmail,
+ detectedSignals: Array.from(new Set(signals)),
+ });
+ }
+ }
+}
+
+function auditNarrative(packet, findings) {
+ for (const section of packet.reviewPacket.sections || []) {
+ if (!["acknowledgements", "conflicts", "team-bio"].includes(section.kind)) continue;
+ const signals = detectIdentity(section.text);
+ if (signals.length > 0 || !section.redacted) {
+ addFinding(findings, "ACKNOWLEDGEMENT_IDENTITY_LEAK", `Review packet section ${section.id} can identify the solver team.`, {
+ sectionId: section.id,
+ kind: section.kind,
+ redacted: Boolean(section.redacted),
+ detectedSignals: signals,
+ });
+ }
+ }
+}
+
+function auditAccess(packet, findings) {
+ for (const grant of packet.accessGrants || []) {
+ if (grant.role === "sponsor" || grant.role === "reviewer") {
+ if (packet.challenge.anonymityMode === "anonymous" && grant.packetVariant !== "redacted") {
+ addFinding(findings, "UNBLINDED_REVIEWER_ROLE", `Access grant ${grant.id} exposes an unredacted packet to ${grant.role}.`, {
+ grantId: grant.id,
+ role: grant.role,
+ principal: grant.principal,
+ packetVariant: grant.packetVariant,
+ });
+ }
+ }
+ }
+}
+
+function auditRedactionEvidence(packet, findings) {
+ const redactionReceipts = new Set((packet.redactionReceipts || []).map((receipt) => receipt.artifactId));
+ for (const artifact of packet.artifacts || []) {
+ if (artifact.requiresRedaction && !redactionReceipts.has(artifact.id)) {
+ addFinding(findings, "REDACTION_EVIDENCE_MISSING", `Artifact ${artifact.id} requires redaction but has no receipt.`, {
+ artifactId: artifact.id,
+ requiredReason: artifact.redactionReason || "identity-bearing metadata",
+ });
+ }
+ }
+}
+
+function auditAuditLogs(packet, findings) {
+ for (const log of packet.auditLogs || []) {
+ if (log.containsIdentity && log.visibility !== "arbitrator-only") {
+ addFinding(findings, "AUDIT_LOG_UNPROTECTED", `Audit log ${log.id} contains identity data outside arbitrator-only visibility.`, {
+ logId: log.id,
+ visibility: log.visibility,
+ containsIdentity: log.containsIdentity,
+ });
+ }
+ }
+}
+
+function summarize(findings) {
+ const severityCounts = { high: 0, medium: 0, low: 0 };
+ for (const finding of findings) severityCounts[finding.severity] += 1;
+ const riskScore = Math.min(100, findings.reduce((sum, finding) => sum + finding.penalty, 0));
+ let decision = "release-redacted-packet";
+ if (severityCounts.high > 0 || riskScore >= 50) decision = "hold-review-packet";
+ else if (severityCounts.medium > 0) decision = "needs-redaction-review";
+ return { severityCounts, riskScore, decision };
+}
+
+function auditAnonymousReviewPacket(packet, options = {}) {
+ if (!packet || typeof packet !== "object") throw new TypeError("packet must be an object");
+ const normalized = {
+ challenge: {},
+ reviewPacket: { sections: [] },
+ artifacts: [],
+ commitHistory: [],
+ accessGrants: [],
+ redactionReceipts: [],
+ auditLogs: [],
+ ...packet,
+ };
+ const findings = [];
+
+ auditChallengeMode(normalized, findings);
+ auditArtifacts(normalized, findings);
+ auditCommits(normalized, findings);
+ auditNarrative(normalized, findings);
+ auditAccess(normalized, findings);
+ auditRedactionEvidence(normalized, findings);
+ auditAuditLogs(normalized, findings);
+
+ const summary = summarize(findings);
+ const evidenceDigest = digest({
+ challengeId: normalized.challenge.id,
+ submissionId: normalized.submissionId,
+ reviewPacket: normalized.reviewPacket,
+ findings,
+ });
+
+ return {
+ submissionId: normalized.submissionId,
+ challengeId: normalized.challenge.id,
+ title: normalized.challenge.title,
+ generatedAt: options.now || "2026-05-28T21:10:00.000Z",
+ decision: summary.decision,
+ riskScore: summary.riskScore,
+ severityCounts: summary.severityCounts,
+ findingCount: findings.length,
+ evidenceDigest,
+ summary: {
+ artifacts: normalized.artifacts.length,
+ commits: normalized.commitHistory.length,
+ accessGrants: normalized.accessGrants.length,
+ redactionReceipts: normalized.redactionReceipts.length,
+ auditLogs: normalized.auditLogs.length,
+ },
+ findings,
+ };
+}
+
+function createMarkdownReport(report) {
+ const findings = report.findings.length
+ ? report.findings
+ .map(
+ (finding, index) => [
+ `### ${index + 1}. ${finding.code} (${finding.severity})`,
+ finding.message,
+ "",
+ `Recommendation: ${finding.recommendation}`,
+ "",
+ "Evidence:",
+ "```json",
+ JSON.stringify(finding.evidence, null, 2),
+ "```",
+ ].join("\n"),
+ )
+ .join("\n\n")
+ : "No blocking findings.";
+
+ return [
+ "# Anonymous Review Packet Guard",
+ "",
+ `Challenge: ${report.title || report.challengeId}`,
+ `Submission: ${report.submissionId}`,
+ `Decision: ${report.decision}`,
+ `Risk score: ${report.riskScore}/100`,
+ `Evidence digest: ${report.evidenceDigest}`,
+ "",
+ "## Scope",
+ "",
+ `Artifacts: ${report.summary.artifacts}`,
+ `Commits: ${report.summary.commits}`,
+ `Access grants: ${report.summary.accessGrants}`,
+ `Redaction receipts: ${report.summary.redactionReceipts}`,
+ `Audit logs: ${report.summary.auditLogs}`,
+ "",
+ "## Findings",
+ "",
+ findings,
+ "",
+ ].join("\n");
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function createSummarySvg(report) {
+ const riskWidth = Math.max(0, Math.min(1, report.riskScore / 100)) * 560;
+ const color = report.decision === "release-redacted-packet" ? "#168a4a" : report.decision === "needs-redaction-review" ? "#b7791f" : "#b91c1c";
+ const rows = report.findings
+ .slice(0, 5)
+ .map(
+ (finding, index) =>
+ `${escapeXml(
+ finding.code,
+ )}: ${escapeXml(finding.message).slice(0, 92)}`,
+ )
+ .join("\n");
+
+ return `
+
+`;
+}
+
+function writeAuditReports(report, outputDir) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ const jsonPath = path.join(outputDir, "anonymous-review-packet-audit.json");
+ const markdownPath = path.join(outputDir, "anonymous-review-packet-audit.md");
+ const svgPath = path.join(outputDir, "anonymous-review-packet-summary.svg");
+ fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`);
+ fs.writeFileSync(markdownPath, createMarkdownReport(report));
+ fs.writeFileSync(svgPath, createSummarySvg(report));
+ return { jsonPath, markdownPath, svgPath };
+}
+
+module.exports = {
+ RULES,
+ auditAnonymousReviewPacket,
+ createMarkdownReport,
+ createSummarySvg,
+ detectIdentity,
+ digest,
+ stableStringify,
+ writeAuditReports,
+};
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/make-demo-video.js b/scientific-bounty-system/anonymous-review-packet-guard/make-demo-video.js
new file mode 100644
index 00000000..29567e05
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/make-demo-video.js
@@ -0,0 +1,154 @@
+const fs = require("fs");
+const path = require("path");
+const { execFileSync } = require("child_process");
+const { auditAnonymousReviewPacket } = require("./index");
+const { samplePacket } = require("./sample-data");
+
+const WIDTH = 1280;
+const HEIGHT = 720;
+const FPS = 12;
+const FRAMES = 48;
+
+const FONT = {
+ " ": ["00000", "00000", "00000", "00000", "00000", "00000", "00000"],
+ "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"],
+ "/": ["00001", "00010", "00010", "00100", "01000", "01000", "10000"],
+ ":": ["00000", "01100", "01100", "00000", "01100", "01100", "00000"],
+ "0": ["01110", "10001", "10011", "10101", "11001", "10001", "01110"],
+ "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
+ "2": ["01110", "10001", "00001", "00010", "00100", "01000", "11111"],
+ "3": ["11110", "00001", "00001", "01110", "00001", "00001", "11110"],
+ "4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
+ "5": ["11111", "10000", "11110", "00001", "00001", "10001", "01110"],
+ "6": ["00110", "01000", "10000", "11110", "10001", "10001", "01110"],
+ "7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
+ "8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
+ "9": ["01110", "10001", "10001", "01111", "00001", "00010", "11100"],
+ A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
+ B: ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
+ C: ["01110", "10001", "10000", "10000", "10000", "10001", "01110"],
+ D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
+ E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
+ F: ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
+ G: ["01110", "10001", "10000", "10111", "10001", "10001", "01111"],
+ H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"],
+ I: ["01110", "00100", "00100", "00100", "00100", "00100", "01110"],
+ J: ["00111", "00010", "00010", "00010", "00010", "10010", "01100"],
+ K: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"],
+ L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
+ M: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
+ N: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
+ O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
+ P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
+ Q: ["01110", "10001", "10001", "10001", "10101", "10010", "01101"],
+ R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
+ S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
+ T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"],
+ U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
+ V: ["10001", "10001", "10001", "10001", "10001", "01010", "00100"],
+ W: ["10001", "10001", "10001", "10101", "10101", "10101", "01010"],
+ X: ["10001", "10001", "01010", "00100", "01010", "10001", "10001"],
+ Y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"],
+ Z: ["11111", "00001", "00010", "00100", "01000", "10000", "11111"],
+};
+
+function color(hex) {
+ return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
+}
+
+function fill(buffer, fillColor) {
+ for (let index = 0; index < buffer.length; index += 3) {
+ buffer[index] = fillColor[0];
+ buffer[index + 1] = fillColor[1];
+ buffer[index + 2] = fillColor[2];
+ }
+}
+
+function rect(buffer, x, y, width, height, fillColor) {
+ const x0 = Math.max(0, Math.floor(x));
+ const y0 = Math.max(0, Math.floor(y));
+ const x1 = Math.min(WIDTH, Math.floor(x + width));
+ const y1 = Math.min(HEIGHT, Math.floor(y + height));
+ for (let row = y0; row < y1; row += 1) {
+ for (let col = x0; col < x1; col += 1) {
+ const index = (row * WIDTH + col) * 3;
+ buffer[index] = fillColor[0];
+ buffer[index + 1] = fillColor[1];
+ buffer[index + 2] = fillColor[2];
+ }
+ }
+}
+
+function text(buffer, phrase, x, y, scale, fillColor) {
+ let cursor = x;
+ for (const rawChar of phrase.toUpperCase()) {
+ const glyph = FONT[rawChar] || FONT[" "];
+ for (let row = 0; row < glyph.length; row += 1) {
+ for (let col = 0; col < glyph[row].length; col += 1) {
+ if (glyph[row][col] === "1") rect(buffer, cursor + col * scale, y + row * scale, scale, scale, fillColor);
+ }
+ }
+ cursor += 6 * scale;
+ }
+}
+
+function writePpm(filePath, buffer) {
+ fs.writeFileSync(filePath, Buffer.concat([Buffer.from(`P6\n${WIDTH} ${HEIGHT}\n255\n`), buffer]));
+}
+
+function frame(frameIndex, report) {
+ const buffer = Buffer.alloc(WIDTH * HEIGHT * 3);
+ fill(buffer, color("#f8fafc"));
+ rect(buffer, 56, 52, 1168, 616, color("#ffffff"));
+ rect(buffer, 56, 52, 1168, 8, color("#b91c1c"));
+ rect(buffer, 92, 112, 18 + frameIndex * 12, 18, color("#b91c1c"));
+ text(buffer, "ANONYMOUS REVIEW PACKET GUARD", 92, 96, 6, color("#0f172a"));
+ text(buffer, `DECISION ${report.decision}`, 92, 184, 5, color("#b91c1c"));
+ text(buffer, `RISK ${report.riskScore}/100`, 92, 250, 5, color("#334155"));
+ text(buffer, `FINDINGS ${report.findingCount} DIGEST ${report.evidenceDigest}`, 92, 310, 4, color("#475569"));
+ rect(buffer, 92, 365, 700, 34, color("#e2e8f0"));
+ rect(buffer, 92, 365, Math.round((report.riskScore / 100) * 700), 34, color("#b91c1c"));
+
+ const lines = ["COMMIT AUTHOR LEAK", "NOTEBOOK PATH LEAK", "DOCUMENT METADATA", "UNBLINDED SPONSOR ACCESS"];
+ for (let index = 0; index < lines.length; index += 1) {
+ const visible = frameIndex > 8 + index * 7;
+ rect(buffer, 92, 442 + index * 44, visible ? 20 : 8, 20, visible ? color("#b91c1c") : color("#cbd5e1"));
+ text(buffer, lines[index], 130, 434 + index * 44, 3, color("#334155"));
+ }
+ text(buffer, "SYNTHETIC CHALLENGE PACKET ONLY", 92, 632, 3, color("#64748b"));
+ return buffer;
+}
+
+const report = auditAnonymousReviewPacket(samplePacket);
+const reportsDir = path.join(__dirname, "reports");
+const frameDir = path.join(reportsDir, "ppm-frames");
+fs.mkdirSync(frameDir, { recursive: true });
+
+for (let index = 0; index < FRAMES; index += 1) {
+ writePpm(path.join(frameDir, `frame-${String(index + 1).padStart(3, "0")}.ppm`), frame(index, report));
+}
+
+const outputPath = path.join(reportsDir, "demo.mp4");
+execFileSync(
+ "ffmpeg",
+ [
+ "-y",
+ "-framerate",
+ String(FPS),
+ "-i",
+ path.join(frameDir, "frame-%03d.ppm"),
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-movflags",
+ "+faststart",
+ outputPath,
+ ],
+ { stdio: "inherit" },
+);
+
+for (const file of fs.readdirSync(frameDir)) fs.unlinkSync(path.join(frameDir, file));
+fs.rmdirSync(frameDir);
+
+console.log(`demo video=${outputPath}`);
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-audit.json b/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-audit.json
new file mode 100644
index 00000000..e565e787
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-audit.json
@@ -0,0 +1,202 @@
+{
+ "submissionId": "submission-anon-biomarker-042",
+ "challengeId": "challenge-biomarker-single-cell",
+ "title": "Anonymous Biomarker Discovery Challenge",
+ "generatedAt": "2026-05-28T21:10:00.000Z",
+ "decision": "hold-review-packet",
+ "riskScore": 100,
+ "severityCounts": {
+ "high": 6,
+ "medium": 6,
+ "low": 0
+ },
+ "findingCount": 12,
+ "evidenceDigest": "2f3128eec95e4284",
+ "summary": {
+ "artifacts": 4,
+ "commits": 2,
+ "accessGrants": 2,
+ "redactionReceipts": 1,
+ "auditLogs": 2
+ },
+ "findings": [
+ {
+ "code": "FILE_PATH_IDENTITY_LEAK",
+ "severity": "high",
+ "penalty": 16,
+ "message": "Artifact notebook-main path contains identity-bearing text.",
+ "evidence": {
+ "artifactId": "notebook-main",
+ "path": "/Users/alice-nguyen/stanford-lab/bounty/submission/main.ipynb",
+ "detectedSignals": [
+ "home-directory",
+ "institution"
+ ]
+ },
+ "recommendation": "Replace local usernames, lab folders, and institution paths with neutral artifact paths."
+ },
+ {
+ "code": "DOCUMENT_METADATA_LEAK",
+ "severity": "high",
+ "penalty": 15,
+ "message": "Artifact notebook-main metadata can identify the solver team.",
+ "evidence": {
+ "artifactId": "notebook-main",
+ "metadata": {
+ "author": "Alice Nguyen",
+ "kernelspec": "python3",
+ "lastModifiedBy": "alice.nguyen@stanford.edu"
+ },
+ "detectedSignals": [
+ "email",
+ "institution",
+ "github-handle"
+ ]
+ },
+ "recommendation": "Strip document author, company, editor, and template metadata from review exports."
+ },
+ {
+ "code": "NOTEBOOK_OUTPUT_IDENTITY_LEAK",
+ "severity": "high",
+ "penalty": 15,
+ "message": "Notebook notebook-main output contains solver identity signals.",
+ "evidence": {
+ "artifactId": "notebook-main",
+ "outputHits": [
+ {
+ "index": 0,
+ "signals": [
+ "home-directory"
+ ],
+ "value": "Loaded cohort from /Users/alice-nguyen/private/cohort.csv"
+ }
+ ]
+ },
+ "recommendation": "Clear notebook outputs or redact user, host, and local path fragments before reviewer access."
+ },
+ {
+ "code": "DOCUMENT_METADATA_LEAK",
+ "severity": "high",
+ "penalty": 15,
+ "message": "Artifact manuscript-pdf metadata can identify the solver team.",
+ "evidence": {
+ "artifactId": "manuscript-pdf",
+ "metadata": {
+ "author": "A. Nguyen",
+ "company": "Stanford University",
+ "template": "lab-letterhead-v4"
+ },
+ "detectedSignals": [
+ "institution"
+ ]
+ },
+ "recommendation": "Strip document author, company, editor, and template metadata from review exports."
+ },
+ {
+ "code": "EXIF_GPS_OR_DEVICE_LEAK",
+ "severity": "medium",
+ "penalty": 10,
+ "message": "Media artifact figure-workflow contains identifying EXIF metadata.",
+ "evidence": {
+ "artifactId": "figure-workflow",
+ "exif": {
+ "gps": "37.4275,-122.1697",
+ "deviceOwner": "Alice Nguyen",
+ "serialNumber": "IMG-ALICE-9981"
+ }
+ },
+ "recommendation": "Remove EXIF GPS, serial, device owner, and capture metadata from figures and media."
+ },
+ {
+ "code": "COMMIT_AUTHOR_LEAK",
+ "severity": "high",
+ "penalty": 18,
+ "message": "Commit a1b2c3d exposes solver identity metadata.",
+ "evidence": {
+ "sha": "a1b2c3d",
+ "authorName": "Alice Nguyen",
+ "authorEmail": "alice.nguyen@stanford.edu",
+ "detectedSignals": [
+ "email",
+ "institution",
+ "github-handle"
+ ]
+ },
+ "recommendation": "Rewrite or mask commit author metadata before the sponsor review packet is released."
+ },
+ {
+ "code": "ACKNOWLEDGEMENT_IDENTITY_LEAK",
+ "severity": "medium",
+ "penalty": 9,
+ "message": "Review packet section acknowledgements can identify the solver team.",
+ "evidence": {
+ "sectionId": "acknowledgements",
+ "kind": "acknowledgements",
+ "redacted": false,
+ "detectedSignals": [
+ "institution",
+ "github-handle"
+ ]
+ },
+ "recommendation": "Move acknowledgements and institution references to the post-award disclosure packet."
+ },
+ {
+ "code": "UNBLINDED_REVIEWER_ROLE",
+ "severity": "high",
+ "penalty": 17,
+ "message": "Access grant grant-sponsor-review exposes an unredacted packet to sponsor.",
+ "evidence": {
+ "grantId": "grant-sponsor-review",
+ "role": "sponsor",
+ "principal": "pharma-sponsor",
+ "packetVariant": "unredacted"
+ },
+ "recommendation": "Remove sponsor/reviewer access until all anonymous review roles use the redacted packet."
+ },
+ {
+ "code": "REDACTION_EVIDENCE_MISSING",
+ "severity": "medium",
+ "penalty": 8,
+ "message": "Artifact notebook-main requires redaction but has no receipt.",
+ "evidence": {
+ "artifactId": "notebook-main",
+ "requiredReason": "local username and lab path in notebook metadata"
+ },
+ "recommendation": "Attach redaction receipts for every transformed artifact before review release."
+ },
+ {
+ "code": "REDACTION_EVIDENCE_MISSING",
+ "severity": "medium",
+ "penalty": 8,
+ "message": "Artifact manuscript-pdf requires redaction but has no receipt.",
+ "evidence": {
+ "artifactId": "manuscript-pdf",
+ "requiredReason": "office metadata contains company field"
+ },
+ "recommendation": "Attach redaction receipts for every transformed artifact before review release."
+ },
+ {
+ "code": "REDACTION_EVIDENCE_MISSING",
+ "severity": "medium",
+ "penalty": 8,
+ "message": "Artifact figure-workflow requires redaction but has no receipt.",
+ "evidence": {
+ "artifactId": "figure-workflow",
+ "requiredReason": "EXIF GPS/device owner present"
+ },
+ "recommendation": "Attach redaction receipts for every transformed artifact before review release."
+ },
+ {
+ "code": "AUDIT_LOG_UNPROTECTED",
+ "severity": "medium",
+ "penalty": 7,
+ "message": "Audit log identity-map contains identity data outside arbitrator-only visibility.",
+ "evidence": {
+ "logId": "identity-map",
+ "visibility": "reviewer-visible",
+ "containsIdentity": true
+ },
+ "recommendation": "Keep full identity audit logs restricted to arbitrators until award or dispute resolution."
+ }
+ ]
+}
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-audit.md b/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-audit.md
new file mode 100644
index 00000000..8aff54ee
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-audit.md
@@ -0,0 +1,219 @@
+# Anonymous Review Packet Guard
+
+Challenge: Anonymous Biomarker Discovery Challenge
+Submission: submission-anon-biomarker-042
+Decision: hold-review-packet
+Risk score: 100/100
+Evidence digest: 2f3128eec95e4284
+
+## Scope
+
+Artifacts: 4
+Commits: 2
+Access grants: 2
+Redaction receipts: 1
+Audit logs: 2
+
+## Findings
+
+### 1. FILE_PATH_IDENTITY_LEAK (high)
+Artifact notebook-main path contains identity-bearing text.
+
+Recommendation: Replace local usernames, lab folders, and institution paths with neutral artifact paths.
+
+Evidence:
+```json
+{
+ "artifactId": "notebook-main",
+ "path": "/Users/alice-nguyen/stanford-lab/bounty/submission/main.ipynb",
+ "detectedSignals": [
+ "home-directory",
+ "institution"
+ ]
+}
+```
+
+### 2. DOCUMENT_METADATA_LEAK (high)
+Artifact notebook-main metadata can identify the solver team.
+
+Recommendation: Strip document author, company, editor, and template metadata from review exports.
+
+Evidence:
+```json
+{
+ "artifactId": "notebook-main",
+ "metadata": {
+ "author": "Alice Nguyen",
+ "kernelspec": "python3",
+ "lastModifiedBy": "alice.nguyen@stanford.edu"
+ },
+ "detectedSignals": [
+ "email",
+ "institution",
+ "github-handle"
+ ]
+}
+```
+
+### 3. NOTEBOOK_OUTPUT_IDENTITY_LEAK (high)
+Notebook notebook-main output contains solver identity signals.
+
+Recommendation: Clear notebook outputs or redact user, host, and local path fragments before reviewer access.
+
+Evidence:
+```json
+{
+ "artifactId": "notebook-main",
+ "outputHits": [
+ {
+ "index": 0,
+ "signals": [
+ "home-directory"
+ ],
+ "value": "Loaded cohort from /Users/alice-nguyen/private/cohort.csv"
+ }
+ ]
+}
+```
+
+### 4. DOCUMENT_METADATA_LEAK (high)
+Artifact manuscript-pdf metadata can identify the solver team.
+
+Recommendation: Strip document author, company, editor, and template metadata from review exports.
+
+Evidence:
+```json
+{
+ "artifactId": "manuscript-pdf",
+ "metadata": {
+ "author": "A. Nguyen",
+ "company": "Stanford University",
+ "template": "lab-letterhead-v4"
+ },
+ "detectedSignals": [
+ "institution"
+ ]
+}
+```
+
+### 5. EXIF_GPS_OR_DEVICE_LEAK (medium)
+Media artifact figure-workflow contains identifying EXIF metadata.
+
+Recommendation: Remove EXIF GPS, serial, device owner, and capture metadata from figures and media.
+
+Evidence:
+```json
+{
+ "artifactId": "figure-workflow",
+ "exif": {
+ "gps": "37.4275,-122.1697",
+ "deviceOwner": "Alice Nguyen",
+ "serialNumber": "IMG-ALICE-9981"
+ }
+}
+```
+
+### 6. COMMIT_AUTHOR_LEAK (high)
+Commit a1b2c3d exposes solver identity metadata.
+
+Recommendation: Rewrite or mask commit author metadata before the sponsor review packet is released.
+
+Evidence:
+```json
+{
+ "sha": "a1b2c3d",
+ "authorName": "Alice Nguyen",
+ "authorEmail": "alice.nguyen@stanford.edu",
+ "detectedSignals": [
+ "email",
+ "institution",
+ "github-handle"
+ ]
+}
+```
+
+### 7. ACKNOWLEDGEMENT_IDENTITY_LEAK (medium)
+Review packet section acknowledgements can identify the solver team.
+
+Recommendation: Move acknowledgements and institution references to the post-award disclosure packet.
+
+Evidence:
+```json
+{
+ "sectionId": "acknowledgements",
+ "kind": "acknowledgements",
+ "redacted": false,
+ "detectedSignals": [
+ "institution",
+ "github-handle"
+ ]
+}
+```
+
+### 8. UNBLINDED_REVIEWER_ROLE (high)
+Access grant grant-sponsor-review exposes an unredacted packet to sponsor.
+
+Recommendation: Remove sponsor/reviewer access until all anonymous review roles use the redacted packet.
+
+Evidence:
+```json
+{
+ "grantId": "grant-sponsor-review",
+ "role": "sponsor",
+ "principal": "pharma-sponsor",
+ "packetVariant": "unredacted"
+}
+```
+
+### 9. REDACTION_EVIDENCE_MISSING (medium)
+Artifact notebook-main requires redaction but has no receipt.
+
+Recommendation: Attach redaction receipts for every transformed artifact before review release.
+
+Evidence:
+```json
+{
+ "artifactId": "notebook-main",
+ "requiredReason": "local username and lab path in notebook metadata"
+}
+```
+
+### 10. REDACTION_EVIDENCE_MISSING (medium)
+Artifact manuscript-pdf requires redaction but has no receipt.
+
+Recommendation: Attach redaction receipts for every transformed artifact before review release.
+
+Evidence:
+```json
+{
+ "artifactId": "manuscript-pdf",
+ "requiredReason": "office metadata contains company field"
+}
+```
+
+### 11. REDACTION_EVIDENCE_MISSING (medium)
+Artifact figure-workflow requires redaction but has no receipt.
+
+Recommendation: Attach redaction receipts for every transformed artifact before review release.
+
+Evidence:
+```json
+{
+ "artifactId": "figure-workflow",
+ "requiredReason": "EXIF GPS/device owner present"
+}
+```
+
+### 12. AUDIT_LOG_UNPROTECTED (medium)
+Audit log identity-map contains identity data outside arbitrator-only visibility.
+
+Recommendation: Keep full identity audit logs restricted to arbitrators until award or dispute resolution.
+
+Evidence:
+```json
+{
+ "logId": "identity-map",
+ "visibility": "reviewer-visible",
+ "containsIdentity": true
+}
+```
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-summary.svg b/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-summary.svg
new file mode 100644
index 00000000..6129ac7e
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/reports/anonymous-review-packet-summary.svg
@@ -0,0 +1,18 @@
+
+
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/reports/demo.mp4 b/scientific-bounty-system/anonymous-review-packet-guard/reports/demo.mp4
new file mode 100644
index 00000000..dc13de92
Binary files /dev/null and b/scientific-bounty-system/anonymous-review-packet-guard/reports/demo.mp4 differ
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/sample-data.js b/scientific-bounty-system/anonymous-review-packet-guard/sample-data.js
new file mode 100644
index 00000000..1f5ae0c8
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/sample-data.js
@@ -0,0 +1,125 @@
+const samplePacket = {
+ submissionId: "submission-anon-biomarker-042",
+ challenge: {
+ id: "challenge-biomarker-single-cell",
+ title: "Anonymous Biomarker Discovery Challenge",
+ anonymityMode: "anonymous",
+ sponsorVisibility: "redacted-until-award",
+ },
+ reviewPacket: {
+ id: "review-packet-042",
+ anonymityMode: "anonymous",
+ variant: "mixed",
+ sections: [
+ {
+ id: "methods-summary",
+ kind: "methods",
+ text: "Pipeline summary for the blinded biomarker model.",
+ redacted: true,
+ },
+ {
+ id: "acknowledgements",
+ kind: "acknowledgements",
+ text: "We thank the Stanford Genomics Core and @labsolver for sample preparation.",
+ redacted: false,
+ },
+ ],
+ },
+ artifacts: [
+ {
+ id: "notebook-main",
+ type: "notebook",
+ path: "/Users/alice-nguyen/stanford-lab/bounty/submission/main.ipynb",
+ requiresRedaction: true,
+ redactionReason: "local username and lab path in notebook metadata",
+ metadata: {
+ author: "Alice Nguyen",
+ kernelspec: "python3",
+ lastModifiedBy: "alice.nguyen@stanford.edu",
+ },
+ outputs: [
+ "Loaded cohort from /Users/alice-nguyen/private/cohort.csv",
+ "Final AUROC 0.83",
+ ],
+ },
+ {
+ id: "manuscript-pdf",
+ type: "document",
+ path: "review/manuscript.pdf",
+ requiresRedaction: true,
+ redactionReason: "office metadata contains company field",
+ metadata: {
+ author: "A. Nguyen",
+ company: "Stanford University",
+ template: "lab-letterhead-v4",
+ },
+ outputs: [],
+ },
+ {
+ id: "figure-workflow",
+ type: "image",
+ path: "figures/workflow.png",
+ requiresRedaction: true,
+ redactionReason: "EXIF GPS/device owner present",
+ metadata: {
+ exif: {
+ gps: "37.4275,-122.1697",
+ deviceOwner: "Alice Nguyen",
+ serialNumber: "IMG-ALICE-9981",
+ },
+ },
+ outputs: [],
+ },
+ {
+ id: "metrics-json",
+ type: "dataset",
+ path: "review/metrics.json",
+ requiresRedaction: false,
+ metadata: {},
+ outputs: [],
+ },
+ ],
+ commitHistory: [
+ {
+ sha: "a1b2c3d",
+ authorName: "Alice Nguyen",
+ authorEmail: "alice.nguyen@stanford.edu",
+ message: "finalize biomarker model",
+ },
+ {
+ sha: "b2c3d4e",
+ authorName: "anon-solver",
+ authorEmail: "",
+ message: "redacted metric export",
+ },
+ ],
+ accessGrants: [
+ {
+ id: "grant-sponsor-review",
+ role: "sponsor",
+ principal: "pharma-sponsor",
+ packetVariant: "unredacted",
+ },
+ {
+ id: "grant-arbitrator",
+ role: "arbitrator",
+ principal: "platform-arbitrator",
+ packetVariant: "identity-ledger",
+ },
+ ],
+ redactionReceipts: [{ artifactId: "metrics-json", transformDigest: "sha256:metrics-noop" }],
+ auditLogs: [
+ {
+ id: "identity-map",
+ containsIdentity: true,
+ visibility: "reviewer-visible",
+ },
+ {
+ id: "arbitrator-ledger",
+ containsIdentity: true,
+ visibility: "arbitrator-only",
+ },
+ ],
+};
+
+module.exports = { samplePacket };
diff --git a/scientific-bounty-system/anonymous-review-packet-guard/test.js b/scientific-bounty-system/anonymous-review-packet-guard/test.js
new file mode 100644
index 00000000..18ba07b1
--- /dev/null
+++ b/scientific-bounty-system/anonymous-review-packet-guard/test.js
@@ -0,0 +1,81 @@
+const assert = require("assert");
+const fs = require("fs");
+const os = require("os");
+const path = require("path");
+const { auditAnonymousReviewPacket, detectIdentity, writeAuditReports } = require("./index");
+const { samplePacket } = require("./sample-data");
+
+function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+function makeReadyPacket() {
+ const packet = clone(samplePacket);
+ packet.reviewPacket.variant = "redacted";
+ packet.reviewPacket.sections = packet.reviewPacket.sections.map((section) => ({
+ ...section,
+ text: section.kind === "acknowledgements" ? "Acknowledgements withheld until award disclosure." : section.text,
+ redacted: true,
+ }));
+ packet.artifacts = packet.artifacts.map((artifact) => ({
+ ...artifact,
+ path: `review/${artifact.id}`,
+ requiresRedaction: false,
+ metadata: artifact.type === "notebook" ? { kernelspec: "python3" } : {},
+ outputs: artifact.type === "notebook" ? ["Loaded blinded cohort from review/cohort.csv", "Final AUROC 0.83"] : [],
+ }));
+ packet.commitHistory = packet.commitHistory.map((commit) => ({
+ ...commit,
+ authorName: "anonymous-solver",
+ authorEmail: "",
+ message: "redacted submission commit",
+ }));
+ packet.accessGrants = packet.accessGrants.map((grant) =>
+ grant.role === "sponsor" ? { ...grant, packetVariant: "redacted" } : grant,
+ );
+ packet.redactionReceipts = packet.artifacts.map((artifact) => ({ artifactId: artifact.id, transformDigest: `sha256:${artifact.id}` }));
+ packet.auditLogs = packet.auditLogs.map((log) => (log.containsIdentity ? { ...log, visibility: "arbitrator-only" } : log));
+ return packet;
+}
+
+function codes(report) {
+ return new Set(report.findings.map((finding) => finding.code));
+}
+
+assert.deepStrictEqual(detectIdentity("alice.nguyen@stanford.edu /Users/alice @labsolver").sort(), [
+ "email",
+ "github-handle",
+ "home-directory",
+ "institution",
+]);
+
+const report = auditAnonymousReviewPacket(samplePacket);
+const findingCodes = codes(report);
+assert.strictEqual(report.decision, "hold-review-packet");
+assert.strictEqual(report.riskScore, 100);
+assert(findingCodes.has("COMMIT_AUTHOR_LEAK"));
+assert(findingCodes.has("FILE_PATH_IDENTITY_LEAK"));
+assert(findingCodes.has("NOTEBOOK_OUTPUT_IDENTITY_LEAK"));
+assert(findingCodes.has("DOCUMENT_METADATA_LEAK"));
+assert(findingCodes.has("EXIF_GPS_OR_DEVICE_LEAK"));
+assert(findingCodes.has("ACKNOWLEDGEMENT_IDENTITY_LEAK"));
+assert(findingCodes.has("UNBLINDED_REVIEWER_ROLE"));
+assert(findingCodes.has("REDACTION_EVIDENCE_MISSING"));
+assert(findingCodes.has("AUDIT_LOG_UNPROTECTED"));
+
+const repeatedReport = auditAnonymousReviewPacket(samplePacket);
+assert.strictEqual(report.evidenceDigest, repeatedReport.evidenceDigest, "audit digest should be deterministic");
+
+const readyReport = auditAnonymousReviewPacket(makeReadyPacket());
+assert.strictEqual(readyReport.decision, "release-redacted-packet");
+assert.strictEqual(readyReport.findingCount, 0);
+assert.strictEqual(readyReport.riskScore, 0);
+
+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "anonymous-review-packet-"));
+const paths = writeAuditReports(report, tempDir);
+for (const outputPath of Object.values(paths)) {
+ assert(fs.existsSync(outputPath), `expected report at ${outputPath}`);
+}
+assert(fs.readFileSync(paths.markdownPath, "utf8").includes("Anonymous Review Packet Guard"));
+
+console.log("anonymous-review-packet-guard tests passed");