diff --git a/peer-review-evidence-binder/README.md b/peer-review-evidence-binder/README.md
new file mode 100644
index 00000000..79f4148f
--- /dev/null
+++ b/peer-review-evidence-binder/README.md
@@ -0,0 +1,36 @@
+# AI Peer Review Evidence Binder
+
+Self-contained guard for SCIBASE issue #16, AI-Powered Research Assistant Suite.
+
+The module validates AI-generated peer-review comments before they are shown to authors. It focuses on the review-output layer: evidence pointers, severity calibration, unsupported accusations, blinded-review privacy, actionable remediation, and deterministic audit evidence.
+
+## What It Checks
+
+- Every AI review finding cites concrete manuscript or artifact evidence.
+- Blocking or severe comments have enough evidence strength to justify their severity.
+- Unsupported accusations and hallucinated review findings are held before release.
+- Blinded-review identity details and private reviewer notes do not leak into author-visible comments.
+- Review comments contain actionable remediation tasks instead of vague criticism.
+- Reviewer-ready JSON, Markdown, SVG, and MP4 artifacts can be regenerated locally.
+
+## Files
+
+- `index.js` - evaluation engine and report formatters
+- `sample-data.js` - synthetic AI review packets
+- `test.js` - dependency-free tests using Node's built-in `assert`
+- `demo.js` - generates reviewer JSON, Markdown, and SVG reports
+- `render-video.js` - renders a short MP4 with `ffmpeg`, or an animated GIF fallback with ImageMagick
+- `reports/demo.mp4` - reviewer demo artifact
+
+## Validation
+
+```bash
+node peer-review-evidence-binder/test.js
+node peer-review-evidence-binder/demo.js
+node peer-review-evidence-binder/render-video.js
+node --check peer-review-evidence-binder/index.js
+node --check peer-review-evidence-binder/sample-data.js
+node --check peer-review-evidence-binder/test.js
+node --check peer-review-evidence-binder/demo.js
+node --check peer-review-evidence-binder/render-video.js
+```
diff --git a/peer-review-evidence-binder/demo.js b/peer-review-evidence-binder/demo.js
new file mode 100644
index 00000000..4d1e60d8
--- /dev/null
+++ b/peer-review-evidence-binder/demo.js
@@ -0,0 +1,16 @@
+const fs = require('fs');
+const path = require('path');
+const { packets } = require('./sample-data');
+const { evaluatePackets, formatMarkdown, formatSvg } = require('./index');
+
+const reportsDir = path.join(__dirname, 'reports');
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const reviewPacket = evaluatePackets(packets);
+
+fs.writeFileSync(path.join(reportsDir, 'peer-review-evidence-report.json'), `${JSON.stringify(reviewPacket, null, 2)}\n`);
+fs.writeFileSync(path.join(reportsDir, 'peer-review-evidence-report.md'), formatMarkdown(reviewPacket));
+fs.writeFileSync(path.join(reportsDir, 'peer-review-evidence-report.svg'), formatSvg(reviewPacket));
+
+console.log(`Generated reports in ${reportsDir}`);
+console.log(`Overall decision: ${reviewPacket.overallDecision}`);
diff --git a/peer-review-evidence-binder/index.js b/peer-review-evidence-binder/index.js
new file mode 100644
index 00000000..deb36b43
--- /dev/null
+++ b/peer-review-evidence-binder/index.js
@@ -0,0 +1,296 @@
+const crypto = require('crypto');
+
+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').slice(0, 16);
+}
+
+function byId(items = []) {
+ return new Map(items.map((item) => [item.id, item]));
+}
+
+function addFinding(findings, severity, code, message, evidence = {}) {
+ findings.push({ severity, code, message, evidence });
+}
+
+function evidenceRank(strength) {
+ return { none: 0, weak: 1, medium: 2, strong: 3 }[strength] || 0;
+}
+
+function severityRank(severity) {
+ return { note: 0, low: 1, medium: 2, high: 3, critical: 4 }[severity] || 0;
+}
+
+function hasAccusationLanguage(text = '') {
+ return /\b(fabricated|fraud|fraudulent|plagiarized|plagiarism|fake|dishonest|misconduct)\b/i.test(text);
+}
+
+function hasIdentityLeak(text = '') {
+ return /reviewer\s*#?\d|reviewer email|@[\w.-]+\.\w+|dr\.\s+[a-z]+|prof\.\s+[a-z]+/i.test(text);
+}
+
+function evaluateReviewPacket(packet) {
+ const findings = [];
+ const artifacts = byId(packet.artifacts);
+ const comments = packet.reviewComments || [];
+
+ if (!packet.assistantRun?.model || !packet.assistantRun?.promptDigest) {
+ addFinding(findings, 'medium', 'assistant_run_not_auditable', 'Assistant run metadata is missing model or prompt digest evidence.');
+ }
+
+ if (comments.length === 0) {
+ addFinding(findings, 'high', 'missing_review_comments', 'AI review packet contains no generated review comments.');
+ }
+
+ for (const comment of comments) {
+ validateEvidencePointers(comment, artifacts, findings);
+ validateSeverityCalibration(comment, findings);
+ validatePrivacy(comment, packet, findings);
+ validateActionability(comment, findings);
+ }
+
+ const high = findings.filter((finding) => finding.severity === 'high').length;
+ const medium = findings.filter((finding) => finding.severity === 'medium').length;
+ const low = findings.filter((finding) => finding.severity === 'low').length;
+ const decision = high > 0 ? 'hold' : medium > 0 ? 'revise' : 'release';
+
+ return {
+ packetId: packet.id,
+ title: packet.title,
+ decision,
+ summary: { high, medium, low, total: findings.length },
+ findings,
+ releaseActions: buildReleaseActions(decision, findings),
+ requirementMap: {
+ autoPeerReview: 'AI-generated review comments are checked before release to authors',
+ evidenceLinkedReports: 'Every finding must cite manuscript, table, figure, code, or data evidence',
+ hallucinationControl: 'Unsupported accusations and missing evidence references force a hold',
+ privacy: 'Blinded-review identity details and private reviewer notes are blocked',
+ reviewerReadyArtifacts: 'Deterministic digests and reports make the review packet auditable'
+ },
+ auditDigest: digest({
+ packet: packet.id,
+ assistantRun: packet.assistantRun,
+ comments,
+ findings
+ })
+ };
+}
+
+function validateEvidencePointers(comment, artifacts, findings) {
+ const refs = comment.evidenceRefs || [];
+ if (refs.length === 0) {
+ addFinding(findings, 'high', 'review_comment_missing_evidence', `${comment.id} has no manuscript or artifact evidence pointers.`, {
+ commentId: comment.id
+ });
+ return;
+ }
+
+ for (const ref of refs) {
+ const artifact = artifacts.get(ref.artifactId);
+ if (!artifact) {
+ addFinding(findings, 'high', 'evidence_artifact_missing', `${comment.id} references missing evidence artifact ${ref.artifactId}.`, {
+ commentId: comment.id,
+ artifactId: ref.artifactId
+ });
+ continue;
+ }
+ if (!ref.locator || !ref.quoteDigest) {
+ addFinding(findings, 'medium', 'evidence_locator_incomplete', `${comment.id} has incomplete evidence locator metadata.`, {
+ commentId: comment.id,
+ artifactId: ref.artifactId
+ });
+ }
+ if (comment.topic && Array.isArray(artifact.topics) && !artifact.topics.includes(comment.topic)) {
+ addFinding(findings, 'medium', 'evidence_topic_mismatch', `${comment.id} cites evidence outside its stated topic.`, {
+ commentId: comment.id,
+ topic: comment.topic,
+ artifactId: artifact.id,
+ artifactTopics: artifact.topics
+ });
+ }
+ }
+}
+
+function validateSeverityCalibration(comment, findings) {
+ const refs = comment.evidenceRefs || [];
+ const bestStrength = Math.max(0, ...refs.map((ref) => evidenceRank(ref.strength)));
+ const rank = severityRank(comment.severity);
+
+ if (rank >= severityRank('high') && bestStrength < evidenceRank('strong')) {
+ addFinding(findings, 'high', 'severity_not_calibrated', `${comment.id} is high severity without strong evidence.`, {
+ commentId: comment.id,
+ severity: comment.severity,
+ bestEvidenceStrength: bestStrength
+ });
+ }
+
+ if (comment.kind === 'blocking' && Number(comment.confidence || 0) < 0.7) {
+ addFinding(findings, 'medium', 'low_confidence_blocker', `${comment.id} blocks release with low assistant confidence.`, {
+ commentId: comment.id,
+ confidence: comment.confidence
+ });
+ }
+
+ if (hasAccusationLanguage(comment.text) && bestStrength < evidenceRank('strong')) {
+ addFinding(findings, 'high', 'unsupported_accusation_language', `${comment.id} uses accusation language without strong evidence.`, {
+ commentId: comment.id
+ });
+ }
+}
+
+function validatePrivacy(comment, packet, findings) {
+ if (comment.containsPrivateNote || comment.visibility === 'internal') {
+ addFinding(findings, 'high', 'private_review_note_leak', `${comment.id} exposes internal review notes to the author-facing packet.`, {
+ commentId: comment.id,
+ visibility: comment.visibility
+ });
+ }
+
+ if (packet.blindReview === true && (comment.mentionsReviewerIdentity || hasIdentityLeak(comment.text))) {
+ addFinding(findings, 'high', 'blind_review_identity_leak', `${comment.id} leaks reviewer identity in a blinded review packet.`, {
+ commentId: comment.id
+ });
+ }
+}
+
+function validateActionability(comment, findings) {
+ const actions = comment.actionItems || [];
+ if (actions.length === 0) {
+ addFinding(findings, 'medium', 'missing_actionable_remediation', `${comment.id} does not give the author an actionable fix.`, {
+ commentId: comment.id
+ });
+ }
+
+ if (comment.text && comment.text.length < 40) {
+ addFinding(findings, 'low', 'review_comment_too_thin', `${comment.id} is too terse for a reviewer-ready author comment.`, {
+ commentId: comment.id
+ });
+ }
+}
+
+function buildReleaseActions(decision, findings) {
+ if (decision === 'release') {
+ return ['Release AI review packet to authors with audit digest attached.'];
+ }
+ const codes = [...new Set(findings.map((finding) => finding.code))];
+ const actions = [];
+ if (codes.includes('review_comment_missing_evidence') || codes.includes('evidence_artifact_missing')) {
+ actions.push('Bind each review comment to concrete manuscript, figure, table, data, or code evidence.');
+ }
+ if (codes.includes('severity_not_calibrated') || codes.includes('unsupported_accusation_language')) {
+ actions.push('Downgrade unsupported severity or add strong evidence before release.');
+ }
+ if (codes.includes('private_review_note_leak') || codes.includes('blind_review_identity_leak')) {
+ actions.push('Redact private reviewer notes and identity details from author-visible output.');
+ }
+ if (codes.includes('missing_actionable_remediation')) {
+ actions.push('Add author-facing remediation tasks for each critique.');
+ }
+ return actions;
+}
+
+function evaluatePackets(packets) {
+ const reviews = packets.map(evaluateReviewPacket);
+ return {
+ generatedAt: new Date().toISOString(),
+ overallDecision: reviews.some((review) => review.decision === 'hold')
+ ? 'hold'
+ : reviews.some((review) => review.decision === 'revise')
+ ? 'revise'
+ : 'release',
+ reviews,
+ packetDigest: digest(reviews)
+ };
+}
+
+function formatMarkdown(packet) {
+ const lines = [
+ '# AI Peer Review Evidence Binder Report',
+ '',
+ `Generated: ${packet.generatedAt}`,
+ `Overall decision: **${packet.overallDecision.toUpperCase()}**`,
+ `Packet digest: \`${packet.packetDigest}\``,
+ ''
+ ];
+
+ for (const review of packet.reviews) {
+ lines.push(`## ${review.title}`);
+ lines.push('');
+ lines.push(`Decision: **${review.decision.toUpperCase()}**`);
+ lines.push(`Audit digest: \`${review.auditDigest}\``);
+ lines.push(`Findings: ${review.summary.total} (${review.summary.high} high, ${review.summary.medium} medium, ${review.summary.low} low)`);
+ lines.push('');
+ if (review.findings.length === 0) {
+ lines.push('- No evidence-binder findings.');
+ } else {
+ for (const finding of review.findings) {
+ lines.push(`- **${finding.severity.toUpperCase()}** \`${finding.code}\`: ${finding.message}`);
+ }
+ }
+ lines.push('');
+ lines.push('Release actions:');
+ for (const action of review.releaseActions) {
+ lines.push(`- ${action}`);
+ }
+ lines.push('');
+ }
+
+ return `${lines.join('\n')}\n`;
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+function formatSvg(packet) {
+ const width = 960;
+ const rowHeight = 88;
+ const height = 170 + packet.reviews.length * rowHeight;
+ const rows = packet.reviews
+ .map((review, index) => {
+ const y = 132 + index * rowHeight;
+ const color = review.decision === 'hold' ? '#dc2626' : review.decision === 'revise' ? '#ca8a04' : '#16a34a';
+ return `
+
+
+
+ ${escapeXml(review.title)}
+ ${review.decision.toUpperCase()} - ${review.summary.total} findings - ${review.auditDigest}
+ `;
+ })
+ .join('\n');
+
+ return `
+`;
+}
+
+module.exports = {
+ digest,
+ evaluateReviewPacket,
+ evaluatePackets,
+ formatMarkdown,
+ formatSvg,
+ stableStringify
+};
diff --git a/peer-review-evidence-binder/render-video.js b/peer-review-evidence-binder/render-video.js
new file mode 100644
index 00000000..dff69b9e
--- /dev/null
+++ b/peer-review-evidence-binder/render-video.js
@@ -0,0 +1,159 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const output = path.join(__dirname, 'reports', 'demo.mp4');
+const gifOutput = path.join(__dirname, 'reports', 'demo.gif');
+
+const WIDTH = 480;
+const HEIGHT = 270;
+
+function ensureReportsDir() {
+ fs.mkdirSync(path.dirname(output), { recursive: true });
+}
+
+function writePpm(filePath, pixels) {
+ const header = Buffer.from(`P6\n${WIDTH} ${HEIGHT}\n255\n`);
+ fs.writeFileSync(filePath, Buffer.concat([header, pixels]));
+}
+
+function rgb(hex) {
+ const clean = hex.replace('#', '');
+ return [0, 2, 4].map((index) => parseInt(clean.slice(index, index + 2), 16));
+}
+
+function makeCanvas(color = '#ffffff') {
+ const canvas = Buffer.alloc(WIDTH * HEIGHT * 3);
+ const [r, g, b] = rgb(color);
+ for (let i = 0; i < canvas.length; i += 3) {
+ canvas[i] = r;
+ canvas[i + 1] = g;
+ canvas[i + 2] = b;
+ }
+ return canvas;
+}
+
+function rect(canvas, x, y, width, height, color) {
+ const [r, g, b] = rgb(color);
+ for (let yy = Math.max(0, y); yy < Math.min(HEIGHT, y + height); yy++) {
+ for (let xx = Math.max(0, x); xx < Math.min(WIDTH, x + width); xx++) {
+ const i = (yy * WIDTH + xx) * 3;
+ canvas[i] = r;
+ canvas[i + 1] = g;
+ canvas[i + 2] = b;
+ }
+ }
+}
+
+const FONT = {
+ A: ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
+ B: ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],
+ C: ['01111', '10000', '10000', '10000', '10000', '10000', '01111'],
+ D: ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],
+ E: ['11111', '10000', '10000', '11110', '10000', '10000', '11111'],
+ G: ['01111', '10000', '10000', '10111', '10001', '10001', '01111'],
+ H: ['10001', '10001', '10001', '11111', '10001', '10001', '10001'],
+ I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'],
+ L: ['10000', '10000', '10000', '10000', '10000', '10000', '11111'],
+ N: ['10001', '11001', '10101', '10011', '10001', '10001', '10001'],
+ O: ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
+ P: ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
+ 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', '01010', '01010', '00100'],
+ W: ['10001', '10001', '10001', '10101', '10101', '10101', '01010'],
+ Y: ['10001', '10001', '01010', '00100', '00100', '00100', '00100'],
+ '1': ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
+ '6': ['01110', '10000', '10000', '11110', '10001', '10001', '01110'],
+ ' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000']
+};
+
+function text(canvas, value, x, y, scale, color) {
+ let cursor = x;
+ for (const char of value.toUpperCase()) {
+ const glyph = FONT[char] || FONT[' '];
+ for (let row = 0; row < glyph.length; row++) {
+ for (let col = 0; col < glyph[row].length; col++) {
+ if (glyph[row][col] === '1') {
+ rect(canvas, cursor + col * scale, y + row * scale, scale, scale, color);
+ }
+ }
+ }
+ cursor += 6 * scale;
+ }
+}
+
+function frame(title, subtitle, bars) {
+ const canvas = makeCanvas('#f8fafc');
+ rect(canvas, 0, 0, WIDTH, 54, '#0f172a');
+ text(canvas, title, 22, 18, 3, '#ffffff');
+ text(canvas, subtitle, 24, 74, 2, '#334155');
+ bars.forEach((bar, index) => {
+ const y = 126 + index * 34;
+ rect(canvas, 34, y, 320, 18, '#e2e8f0');
+ rect(canvas, 34, y, Math.round(320 * bar.value), 18, bar.color);
+ text(canvas, bar.label, 370, y + 2, 2, '#0f172a');
+ });
+ return canvas;
+}
+
+function renderFrames(dir) {
+ const frames = [
+ frame('AI REVIEW BINDER', 'EVIDENCE BEFORE RELEASE', [
+ { label: 'HOLD', value: 0.9, color: '#dc2626' },
+ { label: 'REVISE', value: 0.55, color: '#ca8a04' },
+ { label: 'RELEASE', value: 0.2, color: '#16a34a' }
+ ]),
+ frame('EVIDENCE CHECK', 'NO HALLUCINATED REVIEW', [
+ { label: 'MISSING', value: 0.86, color: '#dc2626' },
+ { label: 'WEAK', value: 0.56, color: '#ca8a04' },
+ { label: 'STRONG', value: 0.24, color: '#16a34a' }
+ ]),
+ frame('SEVERITY PRIVACY', 'CALIBRATE AND REDACT', [
+ { label: 'SEVERE', value: 0.72, color: '#dc2626' },
+ { label: 'PRIVATE', value: 0.44, color: '#ca8a04' },
+ { label: 'SAFE', value: 0.22, color: '#16a34a' }
+ ]),
+ frame('SCIBASE ISSUE 16', 'HOLD REVISE RELEASE', [
+ { label: 'JSON', value: 0.84, color: '#0ea5e9' },
+ { label: 'MARKDOWN', value: 0.72, color: '#6366f1' },
+ { label: 'SVG MP4', value: 0.64, color: '#10b981' }
+ ])
+ ];
+
+ return frames.map((canvas, index) => {
+ const filePath = path.join(dir, `frame-${String(index + 1).padStart(2, '0')}.ppm`);
+ writePpm(filePath, canvas);
+ return filePath;
+ });
+}
+
+function renderVideo(frames) {
+ const input = path.join(path.dirname(frames[0]), 'frame-%02d.ppm');
+ const ffmpeg = spawnSync('ffmpeg', ['-y', '-framerate', '1', '-i', input, '-vf', 'scale=960:540:flags=neighbor,fps=24,format=yuv420p', '-movflags', '+faststart', output], {
+ stdio: 'inherit'
+ });
+ if (ffmpeg.status === 0) {
+ console.log(`Rendered ${output}`);
+ return;
+ }
+
+ const magick = spawnSync('magick', ['-delay', '140', '-loop', '0', ...frames, gifOutput], {
+ stdio: 'inherit'
+ });
+ if (magick.status !== 0) {
+ throw new Error('Unable to render MP4 with ffmpeg or GIF with ImageMagick');
+ }
+ console.log(`Rendered ${gifOutput}`);
+}
+
+ensureReportsDir();
+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'peer-review-evidence-binder-'));
+try {
+ renderVideo(renderFrames(tmp));
+} finally {
+ fs.rmSync(tmp, { recursive: true, force: true });
+}
diff --git a/peer-review-evidence-binder/reports/demo.mp4 b/peer-review-evidence-binder/reports/demo.mp4
new file mode 100644
index 00000000..ce80bad1
Binary files /dev/null and b/peer-review-evidence-binder/reports/demo.mp4 differ
diff --git a/peer-review-evidence-binder/reports/peer-review-evidence-report.json b/peer-review-evidence-binder/reports/peer-review-evidence-report.json
new file mode 100644
index 00000000..1e3a0045
--- /dev/null
+++ b/peer-review-evidence-binder/reports/peer-review-evidence-report.json
@@ -0,0 +1,198 @@
+{
+ "generatedAt": "2026-05-31T06:08:32.018Z",
+ "overallDecision": "hold",
+ "reviews": [
+ {
+ "packetId": "packet-hold-001",
+ "title": "Unsupported Sepsis Biomarker Review",
+ "decision": "hold",
+ "summary": {
+ "high": 7,
+ "medium": 2,
+ "low": 1,
+ "total": 10
+ },
+ "findings": [
+ {
+ "severity": "high",
+ "code": "review_comment_missing_evidence",
+ "message": "comment-1 has no manuscript or artifact evidence pointers.",
+ "evidence": {
+ "commentId": "comment-1"
+ }
+ },
+ {
+ "severity": "high",
+ "code": "severity_not_calibrated",
+ "message": "comment-1 is high severity without strong evidence.",
+ "evidence": {
+ "commentId": "comment-1",
+ "severity": "critical",
+ "bestEvidenceStrength": 0
+ }
+ },
+ {
+ "severity": "medium",
+ "code": "low_confidence_blocker",
+ "message": "comment-1 blocks release with low assistant confidence.",
+ "evidence": {
+ "commentId": "comment-1",
+ "confidence": 0.42
+ }
+ },
+ {
+ "severity": "high",
+ "code": "unsupported_accusation_language",
+ "message": "comment-1 uses accusation language without strong evidence.",
+ "evidence": {
+ "commentId": "comment-1"
+ }
+ },
+ {
+ "severity": "high",
+ "code": "private_review_note_leak",
+ "message": "comment-1 exposes internal review notes to the author-facing packet.",
+ "evidence": {
+ "commentId": "comment-1",
+ "visibility": "author"
+ }
+ },
+ {
+ "severity": "high",
+ "code": "blind_review_identity_leak",
+ "message": "comment-1 leaks reviewer identity in a blinded review packet.",
+ "evidence": {
+ "commentId": "comment-1"
+ }
+ },
+ {
+ "severity": "medium",
+ "code": "missing_actionable_remediation",
+ "message": "comment-1 does not give the author an actionable fix.",
+ "evidence": {
+ "commentId": "comment-1"
+ }
+ },
+ {
+ "severity": "high",
+ "code": "evidence_artifact_missing",
+ "message": "comment-2 references missing evidence artifact missing-appendix.",
+ "evidence": {
+ "commentId": "comment-2",
+ "artifactId": "missing-appendix"
+ }
+ },
+ {
+ "severity": "high",
+ "code": "severity_not_calibrated",
+ "message": "comment-2 is high severity without strong evidence.",
+ "evidence": {
+ "commentId": "comment-2",
+ "severity": "high",
+ "bestEvidenceStrength": 1
+ }
+ },
+ {
+ "severity": "low",
+ "code": "review_comment_too_thin",
+ "message": "comment-2 is too terse for a reviewer-ready author comment.",
+ "evidence": {
+ "commentId": "comment-2"
+ }
+ }
+ ],
+ "releaseActions": [
+ "Bind each review comment to concrete manuscript, figure, table, data, or code evidence.",
+ "Downgrade unsupported severity or add strong evidence before release.",
+ "Redact private reviewer notes and identity details from author-visible output.",
+ "Add author-facing remediation tasks for each critique."
+ ],
+ "requirementMap": {
+ "autoPeerReview": "AI-generated review comments are checked before release to authors",
+ "evidenceLinkedReports": "Every finding must cite manuscript, table, figure, code, or data evidence",
+ "hallucinationControl": "Unsupported accusations and missing evidence references force a hold",
+ "privacy": "Blinded-review identity details and private reviewer notes are blocked",
+ "reviewerReadyArtifacts": "Deterministic digests and reports make the review packet auditable"
+ },
+ "auditDigest": "48ca305f18af34ea"
+ },
+ {
+ "packetId": "packet-revise-002",
+ "title": "Incomplete Neuroimaging Review",
+ "decision": "revise",
+ "summary": {
+ "high": 0,
+ "medium": 3,
+ "low": 0,
+ "total": 3
+ },
+ "findings": [
+ {
+ "severity": "medium",
+ "code": "evidence_topic_mismatch",
+ "message": "comment-3 cites evidence outside its stated topic.",
+ "evidence": {
+ "commentId": "comment-3",
+ "topic": "methods",
+ "artifactId": "figure-qc",
+ "artifactTopics": [
+ "quality-control"
+ ]
+ }
+ },
+ {
+ "severity": "medium",
+ "code": "low_confidence_blocker",
+ "message": "comment-3 blocks release with low assistant confidence.",
+ "evidence": {
+ "commentId": "comment-3",
+ "confidence": 0.58
+ }
+ },
+ {
+ "severity": "medium",
+ "code": "missing_actionable_remediation",
+ "message": "comment-3 does not give the author an actionable fix.",
+ "evidence": {
+ "commentId": "comment-3"
+ }
+ }
+ ],
+ "releaseActions": [
+ "Add author-facing remediation tasks for each critique."
+ ],
+ "requirementMap": {
+ "autoPeerReview": "AI-generated review comments are checked before release to authors",
+ "evidenceLinkedReports": "Every finding must cite manuscript, table, figure, code, or data evidence",
+ "hallucinationControl": "Unsupported accusations and missing evidence references force a hold",
+ "privacy": "Blinded-review identity details and private reviewer notes are blocked",
+ "reviewerReadyArtifacts": "Deterministic digests and reports make the review packet auditable"
+ },
+ "auditDigest": "9d100b472ff37be1"
+ },
+ {
+ "packetId": "packet-release-003",
+ "title": "Release-Ready Materials Review",
+ "decision": "release",
+ "summary": {
+ "high": 0,
+ "medium": 0,
+ "low": 0,
+ "total": 0
+ },
+ "findings": [],
+ "releaseActions": [
+ "Release AI review packet to authors with audit digest attached."
+ ],
+ "requirementMap": {
+ "autoPeerReview": "AI-generated review comments are checked before release to authors",
+ "evidenceLinkedReports": "Every finding must cite manuscript, table, figure, code, or data evidence",
+ "hallucinationControl": "Unsupported accusations and missing evidence references force a hold",
+ "privacy": "Blinded-review identity details and private reviewer notes are blocked",
+ "reviewerReadyArtifacts": "Deterministic digests and reports make the review packet auditable"
+ },
+ "auditDigest": "71cf1006d4db929b"
+ }
+ ],
+ "packetDigest": "c1eb5bdf69d31590"
+}
diff --git a/peer-review-evidence-binder/reports/peer-review-evidence-report.md b/peer-review-evidence-binder/reports/peer-review-evidence-report.md
new file mode 100644
index 00000000..28da79ce
--- /dev/null
+++ b/peer-review-evidence-binder/reports/peer-review-evidence-report.md
@@ -0,0 +1,53 @@
+# AI Peer Review Evidence Binder Report
+
+Generated: 2026-05-31T06:08:32.018Z
+Overall decision: **HOLD**
+Packet digest: `c1eb5bdf69d31590`
+
+## Unsupported Sepsis Biomarker Review
+
+Decision: **HOLD**
+Audit digest: `48ca305f18af34ea`
+Findings: 10 (7 high, 2 medium, 1 low)
+
+- **HIGH** `review_comment_missing_evidence`: comment-1 has no manuscript or artifact evidence pointers.
+- **HIGH** `severity_not_calibrated`: comment-1 is high severity without strong evidence.
+- **MEDIUM** `low_confidence_blocker`: comment-1 blocks release with low assistant confidence.
+- **HIGH** `unsupported_accusation_language`: comment-1 uses accusation language without strong evidence.
+- **HIGH** `private_review_note_leak`: comment-1 exposes internal review notes to the author-facing packet.
+- **HIGH** `blind_review_identity_leak`: comment-1 leaks reviewer identity in a blinded review packet.
+- **MEDIUM** `missing_actionable_remediation`: comment-1 does not give the author an actionable fix.
+- **HIGH** `evidence_artifact_missing`: comment-2 references missing evidence artifact missing-appendix.
+- **HIGH** `severity_not_calibrated`: comment-2 is high severity without strong evidence.
+- **LOW** `review_comment_too_thin`: comment-2 is too terse for a reviewer-ready author comment.
+
+Release actions:
+- Bind each review comment to concrete manuscript, figure, table, data, or code evidence.
+- Downgrade unsupported severity or add strong evidence before release.
+- Redact private reviewer notes and identity details from author-visible output.
+- Add author-facing remediation tasks for each critique.
+
+## Incomplete Neuroimaging Review
+
+Decision: **REVISE**
+Audit digest: `9d100b472ff37be1`
+Findings: 3 (0 high, 3 medium, 0 low)
+
+- **MEDIUM** `evidence_topic_mismatch`: comment-3 cites evidence outside its stated topic.
+- **MEDIUM** `low_confidence_blocker`: comment-3 blocks release with low assistant confidence.
+- **MEDIUM** `missing_actionable_remediation`: comment-3 does not give the author an actionable fix.
+
+Release actions:
+- Add author-facing remediation tasks for each critique.
+
+## Release-Ready Materials Review
+
+Decision: **RELEASE**
+Audit digest: `71cf1006d4db929b`
+Findings: 0 (0 high, 0 medium, 0 low)
+
+- No evidence-binder findings.
+
+Release actions:
+- Release AI review packet to authors with audit digest attached.
+
diff --git a/peer-review-evidence-binder/reports/peer-review-evidence-report.svg b/peer-review-evidence-binder/reports/peer-review-evidence-report.svg
new file mode 100644
index 00000000..5fee46cb
--- /dev/null
+++ b/peer-review-evidence-binder/reports/peer-review-evidence-report.svg
@@ -0,0 +1,26 @@
+
diff --git a/peer-review-evidence-binder/sample-data.js b/peer-review-evidence-binder/sample-data.js
new file mode 100644
index 00000000..7547aa96
--- /dev/null
+++ b/peer-review-evidence-binder/sample-data.js
@@ -0,0 +1,186 @@
+const packets = [
+ {
+ id: 'packet-hold-001',
+ title: 'Unsupported Sepsis Biomarker Review',
+ blindReview: true,
+ assistantRun: {
+ model: 'scibase-review-assistant-v0',
+ promptDigest: 'prompt-8a9f31',
+ generatedAt: '2026-05-31T06:55:00Z'
+ },
+ artifacts: [
+ {
+ id: 'manuscript-main',
+ type: 'manuscript',
+ path: 'manuscript.md',
+ checksum: 'sha256-manuscript-a',
+ topics: ['biomarkers', 'cohort', 'statistics']
+ },
+ {
+ id: 'table-cohort',
+ type: 'table',
+ path: 'tables/cohort.csv',
+ checksum: 'sha256-table-a',
+ topics: ['cohort']
+ }
+ ],
+ reviewComments: [
+ {
+ id: 'comment-1',
+ kind: 'blocking',
+ severity: 'critical',
+ topic: 'statistics',
+ confidence: 0.42,
+ text: 'Dr. Lane says the authors fabricated samples and this should be rejected.',
+ visibility: 'author',
+ mentionsReviewerIdentity: true,
+ containsPrivateNote: true,
+ evidenceRefs: [],
+ actionItems: []
+ },
+ {
+ id: 'comment-2',
+ kind: 'finding',
+ severity: 'high',
+ topic: 'statistics',
+ confidence: 0.66,
+ text: 'The survival model is invalid.',
+ visibility: 'author',
+ evidenceRefs: [
+ {
+ artifactId: 'missing-appendix',
+ locator: 'Appendix S2',
+ quoteDigest: null,
+ strength: 'weak'
+ }
+ ],
+ actionItems: ['Attach the survival model appendix or remove the claim.']
+ }
+ ]
+ },
+ {
+ id: 'packet-revise-002',
+ title: 'Incomplete Neuroimaging Review',
+ blindReview: false,
+ assistantRun: {
+ model: 'scibase-review-assistant-v0',
+ promptDigest: 'prompt-1d42bf',
+ generatedAt: '2026-05-31T06:57:00Z'
+ },
+ artifacts: [
+ {
+ id: 'manuscript-main',
+ type: 'manuscript',
+ path: 'manuscript.md',
+ checksum: 'sha256-manuscript-b',
+ topics: ['neuroimaging', 'methods']
+ },
+ {
+ id: 'figure-qc',
+ type: 'figure',
+ path: 'figures/qc.png',
+ checksum: 'sha256-figure-b',
+ topics: ['quality-control']
+ }
+ ],
+ reviewComments: [
+ {
+ id: 'comment-3',
+ kind: 'blocking',
+ severity: 'medium',
+ topic: 'methods',
+ confidence: 0.58,
+ text: 'The motion exclusion threshold should be explained before the manuscript is sent out.',
+ visibility: 'author',
+ evidenceRefs: [
+ {
+ artifactId: 'figure-qc',
+ locator: 'Figure 2 caption',
+ quoteDigest: 'quote-ff1930',
+ strength: 'medium'
+ }
+ ],
+ actionItems: []
+ }
+ ]
+ },
+ {
+ id: 'packet-release-003',
+ title: 'Release-Ready Materials Review',
+ blindReview: true,
+ assistantRun: {
+ model: 'scibase-review-assistant-v0',
+ promptDigest: 'prompt-e4c219',
+ generatedAt: '2026-05-31T07:00:00Z'
+ },
+ artifacts: [
+ {
+ id: 'manuscript-main',
+ type: 'manuscript',
+ path: 'manuscript.md',
+ checksum: 'sha256-manuscript-c',
+ topics: ['materials', 'synthesis', 'statistics']
+ },
+ {
+ id: 'table-yield',
+ type: 'table',
+ path: 'tables/yield.csv',
+ checksum: 'sha256-table-c',
+ topics: ['statistics']
+ },
+ {
+ id: 'notebook-analysis',
+ type: 'notebook',
+ path: 'analysis/yield.ipynb',
+ checksum: 'sha256-notebook-c',
+ topics: ['statistics', 'reproducibility']
+ }
+ ],
+ reviewComments: [
+ {
+ id: 'comment-4',
+ kind: 'finding',
+ severity: 'medium',
+ topic: 'statistics',
+ confidence: 0.88,
+ text: 'The yield comparison should report confidence intervals alongside the p-value.',
+ visibility: 'author',
+ evidenceRefs: [
+ {
+ artifactId: 'table-yield',
+ locator: 'Rows 8-14',
+ quoteDigest: 'quote-12aa8e',
+ strength: 'strong'
+ },
+ {
+ artifactId: 'notebook-analysis',
+ locator: 'Cell 11',
+ quoteDigest: 'quote-91bf33',
+ strength: 'strong'
+ }
+ ],
+ actionItems: ['Add 95% confidence intervals for the primary yield comparison.']
+ },
+ {
+ id: 'comment-5',
+ kind: 'finding',
+ severity: 'low',
+ topic: 'synthesis',
+ confidence: 0.82,
+ text: 'The synthesis section should link the catalyst lot number to the supplementary table.',
+ visibility: 'author',
+ evidenceRefs: [
+ {
+ artifactId: 'manuscript-main',
+ locator: 'Methods paragraph 3',
+ quoteDigest: 'quote-4e1021',
+ strength: 'medium'
+ }
+ ],
+ actionItems: ['Add the catalyst lot reference to the supplementary materials link.']
+ }
+ ]
+ }
+];
+
+module.exports = { packets };
diff --git a/peer-review-evidence-binder/test.js b/peer-review-evidence-binder/test.js
new file mode 100644
index 00000000..58e841a9
--- /dev/null
+++ b/peer-review-evidence-binder/test.js
@@ -0,0 +1,49 @@
+const assert = require('assert');
+const { packets } = require('./sample-data');
+const { evaluateReviewPacket, evaluatePackets, formatMarkdown, formatSvg } = require('./index');
+
+function testUnsafeReviewPacketIsHeld() {
+ const review = evaluateReviewPacket(packets[0]);
+ assert.strictEqual(review.decision, 'hold');
+ assert(review.findings.some((finding) => finding.code === 'review_comment_missing_evidence'));
+ assert(review.findings.some((finding) => finding.code === 'unsupported_accusation_language'));
+ assert(review.findings.some((finding) => finding.code === 'private_review_note_leak'));
+ assert(review.findings.some((finding) => finding.code === 'blind_review_identity_leak'));
+}
+
+function testIncompletePacketRequiresRevision() {
+ const review = evaluateReviewPacket(packets[1]);
+ assert.strictEqual(review.decision, 'revise');
+ assert(review.findings.some((finding) => finding.code === 'low_confidence_blocker'));
+ assert(review.findings.some((finding) => finding.code === 'missing_actionable_remediation'));
+}
+
+function testCleanPacketCanRelease() {
+ const review = evaluateReviewPacket(packets[2]);
+ assert.strictEqual(review.decision, 'release');
+ assert.strictEqual(review.findings.length, 0);
+ assert.deepStrictEqual(review.releaseActions, ['Release AI review packet to authors with audit digest attached.']);
+}
+
+function testPacketAndFormats() {
+ const packet = evaluatePackets(packets);
+ assert.strictEqual(packet.overallDecision, 'hold');
+ assert.strictEqual(packet.reviews.length, 3);
+ assert.match(packet.packetDigest, /^[a-f0-9]{16}$/);
+ assert(formatMarkdown(packet).includes('AI Peer Review Evidence Binder Report'));
+ assert(formatSvg(packet).includes('