diff --git a/collab-cover-letter-export-guard/README.md b/collab-cover-letter-export-guard/README.md
new file mode 100644
index 00000000..cba46bea
--- /dev/null
+++ b/collab-cover-letter-export-guard/README.md
@@ -0,0 +1,33 @@
+# Collaborative Cover-Letter Export Guard
+
+Self-contained reviewer artifact for SCIBASE issue #12, focused on the last step before a collaborative manuscript export includes its journal cover letter.
+
+The guard evaluates synthetic submission packets for:
+
+- stale manuscript and cover-letter hashes after final collaborative edits
+- missing or stale corresponding-author approval
+- coauthor approval drift after the final manuscript version
+- novelty, ethics, data availability, and conflict statement mismatches
+- private reviewer or collaborator notes leaking into the submission-facing cover letter
+
+It emits deterministic `RELEASE`, `REVIEW`, or `HOLD` decisions with JSON, Markdown, SVG, and MP4 artifacts. It uses no credentials, no private manuscripts, no external APIs, and no live editor service.
+
+## Requirement Mapping
+
+| Issue #12 capability | Covered here |
+| --- | --- |
+| Inline comments, suggestions, and change tracking | Blocks cover-letter export when private or unresolved collaborator notes are included |
+| Version history and autosave | Requires cover-letter and approvals to match the final manuscript hash |
+| Integrated task workflow | Produces remediation actions for missing approvals and claim drift |
+| Scientific formatting and publication workflow | Validates the journal-facing correspondence packet before export |
+
+## Local Validation
+
+```bash
+npm run check
+npm test
+npm run demo
+npm run demo:video
+```
+
+Generated artifacts are written to `reports/`.
diff --git a/collab-cover-letter-export-guard/demo.js b/collab-cover-letter-export-guard/demo.js
new file mode 100644
index 00000000..201dd25b
--- /dev/null
+++ b/collab-cover-letter-export-guard/demo.js
@@ -0,0 +1,29 @@
+import fs from "node:fs";
+import path from "node:path";
+import { cleanSubmissionPacket, riskySubmissionPacket } from "./src/samplePackets.js";
+import { evaluateCoverLetterExportPacket, renderMarkdownReport } from "./src/guard.js";
+
+const reportsDir = path.join(process.cwd(), "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const cleanResult = evaluateCoverLetterExportPacket(cleanSubmissionPacket);
+const riskyResult = evaluateCoverLetterExportPacket(riskySubmissionPacket);
+
+fs.writeFileSync(path.join(reportsDir, "clean-review.json"), `${JSON.stringify(cleanResult, null, 2)}\n`);
+fs.writeFileSync(path.join(reportsDir, "risky-review.json"), `${JSON.stringify(riskyResult, null, 2)}\n`);
+fs.writeFileSync(path.join(reportsDir, "risky-review.md"), renderMarkdownReport(riskySubmissionPacket, riskyResult));
+
+const svg = `
+`;
+fs.writeFileSync(path.join(reportsDir, "summary.svg"), svg);
+
+console.log(`Clean decision: ${cleanResult.decision}`);
+console.log(`Risky decision: ${riskyResult.decision}`);
+console.log(`Wrote reports to ${reportsDir}`);
diff --git a/collab-cover-letter-export-guard/make-demo-video.js b/collab-cover-letter-export-guard/make-demo-video.js
new file mode 100644
index 00000000..00f8982e
--- /dev/null
+++ b/collab-cover-letter-export-guard/make-demo-video.js
@@ -0,0 +1,50 @@
+import { spawnSync } from "node:child_process";
+import path from "node:path";
+import { riskySubmissionPacket } from "./src/samplePackets.js";
+import { evaluateCoverLetterExportPacket } from "./src/guard.js";
+
+const reportsDir = path.join(process.cwd(), "reports");
+const demoMp4 = path.join(reportsDir, "demo.mp4");
+const resultPacket = evaluateCoverLetterExportPacket(riskySubmissionPacket);
+
+function escapeDrawtext(text) {
+ return text.replaceAll("\\", "\\\\").replaceAll(":", "\\:").replaceAll("'", "\\'");
+}
+
+const font = "C\\:/Windows/Fonts/arial.ttf";
+const lines = [
+ "Collaborative Cover-Letter Export Guard",
+ `Decision ${resultPacket.decision} | Findings ${resultPacket.findingCount}`,
+ "Blocks stale hashes, approval drift, claim mismatches, and private-note leakage.",
+ `Audit digest ${resultPacket.auditDigest}`,
+];
+const drawText = lines
+ .map(
+ (line, index) =>
+ `drawtext=fontfile='${font}':text='${escapeDrawtext(line)}':x=48:y=${64 + index * 72}:fontsize=${index === 0 ? 34 : 24}:fontcolor=${index === 1 ? "0xffdddd" : "white"}`,
+ )
+ .join(",");
+
+const result = spawnSync(
+ "ffmpeg",
+ [
+ "-y",
+ "-f",
+ "lavfi",
+ "-i",
+ "color=c=0x101827:s=960x540:r=12",
+ "-t",
+ "4",
+ "-vf",
+ `${drawText},format=yuv420p`,
+ "-an",
+ demoMp4,
+ ],
+ { encoding: "utf8" },
+);
+
+if (result.status !== 0) {
+ throw new Error(result.stderr || "ffmpeg failed to render demo.mp4");
+}
+
+console.log(`Wrote ${path.relative(process.cwd(), demoMp4)}`);
diff --git a/collab-cover-letter-export-guard/package.json b/collab-cover-letter-export-guard/package.json
new file mode 100644
index 00000000..e49510d4
--- /dev/null
+++ b/collab-cover-letter-export-guard/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "collab-cover-letter-export-guard",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "Dependency-free guard for collaborative journal cover-letter export readiness.",
+ "scripts": {
+ "check": "node --check src/guard.js && node --check src/samplePackets.js && node --check test.js && node --check demo.js && node --check make-demo-video.js",
+ "test": "node test.js",
+ "demo": "node demo.js",
+ "demo:video": "node make-demo-video.js"
+ }
+}
diff --git a/collab-cover-letter-export-guard/reports/clean-review.json b/collab-cover-letter-export-guard/reports/clean-review.json
new file mode 100644
index 00000000..6e0d85d6
--- /dev/null
+++ b/collab-cover-letter-export-guard/reports/clean-review.json
@@ -0,0 +1,10 @@
+{
+ "packetId": "submission-ready-cover-letter",
+ "decision": "RELEASE",
+ "findingCount": 0,
+ "highCount": 0,
+ "mediumCount": 0,
+ "findings": [],
+ "exportReady": true,
+ "auditDigest": "b987f52224b801a1"
+}
diff --git a/collab-cover-letter-export-guard/reports/demo.mp4 b/collab-cover-letter-export-guard/reports/demo.mp4
new file mode 100644
index 00000000..6d1275ba
Binary files /dev/null and b/collab-cover-letter-export-guard/reports/demo.mp4 differ
diff --git a/collab-cover-letter-export-guard/reports/risky-review.json b/collab-cover-letter-export-guard/reports/risky-review.json
new file mode 100644
index 00000000..92f7bd05
--- /dev/null
+++ b/collab-cover-letter-export-guard/reports/risky-review.json
@@ -0,0 +1,59 @@
+{
+ "packetId": "submission-blocked-cover-letter",
+ "decision": "HOLD",
+ "findingCount": 8,
+ "highCount": 5,
+ "mediumCount": 3,
+ "findings": [
+ {
+ "severity": "high",
+ "code": "STALE_COVER_LETTER_HASH",
+ "message": "Cover letter references ms-v10-21bd, but final manuscript is ms-v11-aa30.",
+ "remediation": "Regenerate the cover-letter packet from the final manuscript version."
+ },
+ {
+ "severity": "high",
+ "code": "MISSING_CORRESPONDING_AUTHOR_APPROVAL",
+ "message": "Corresponding author approval is missing or tied to an older manuscript hash.",
+ "remediation": "Collect a fresh corresponding-author approval after the final edit."
+ },
+ {
+ "severity": "medium",
+ "code": "COAUTHOR_APPROVAL_DRIFT",
+ "message": "Missing fresh cover-letter approval from author-2, author-3.",
+ "remediation": "Route the final cover-letter packet to every coauthor who is listed on the manuscript."
+ },
+ {
+ "severity": "medium",
+ "code": "CLAIM_MISMATCH_NOVELTY",
+ "message": "Cover-letter novelty statement diverges from the manuscript claim.",
+ "remediation": "Align cover-letter statements with the final manuscript disclosure text before export."
+ },
+ {
+ "severity": "high",
+ "code": "CLAIM_MISMATCH_ETHICS",
+ "message": "Cover-letter ethics statement diverges from the manuscript claim.",
+ "remediation": "Align cover-letter statements with the final manuscript disclosure text before export."
+ },
+ {
+ "severity": "medium",
+ "code": "CLAIM_MISMATCH_DATAAVAILABILITY",
+ "message": "Cover-letter dataAvailability statement diverges from the manuscript claim.",
+ "remediation": "Align cover-letter statements with the final manuscript disclosure text before export."
+ },
+ {
+ "severity": "high",
+ "code": "CLAIM_MISMATCH_CONFLICTS",
+ "message": "Cover-letter conflicts statement diverges from the manuscript claim.",
+ "remediation": "Align cover-letter statements with the final manuscript disclosure text before export."
+ },
+ {
+ "severity": "high",
+ "code": "PRIVATE_COMMENT_LEAK",
+ "message": "Cover-letter export includes private reviewer or collaborator notes.",
+ "remediation": "Redact private comments and rebuild the editor-facing correspondence packet."
+ }
+ ],
+ "exportReady": false,
+ "auditDigest": "fa958c5cf99c0caa"
+}
diff --git a/collab-cover-letter-export-guard/reports/risky-review.md b/collab-cover-letter-export-guard/reports/risky-review.md
new file mode 100644
index 00000000..0822060b
--- /dev/null
+++ b/collab-cover-letter-export-guard/reports/risky-review.md
@@ -0,0 +1,25 @@
+# Cover Letter Export Guard Report
+
+- Packet: submission-blocked-cover-letter
+- Manuscript: Model-assisted detection of assay drift in synthetic submissions
+- Decision: HOLD
+- Audit digest: fa958c5cf99c0caa
+
+## Findings
+
+- HIGH STALE_COVER_LETTER_HASH: Cover letter references ms-v10-21bd, but final manuscript is ms-v11-aa30.
+ Remediation: Regenerate the cover-letter packet from the final manuscript version.
+- HIGH MISSING_CORRESPONDING_AUTHOR_APPROVAL: Corresponding author approval is missing or tied to an older manuscript hash.
+ Remediation: Collect a fresh corresponding-author approval after the final edit.
+- MEDIUM COAUTHOR_APPROVAL_DRIFT: Missing fresh cover-letter approval from author-2, author-3.
+ Remediation: Route the final cover-letter packet to every coauthor who is listed on the manuscript.
+- MEDIUM CLAIM_MISMATCH_NOVELTY: Cover-letter novelty statement diverges from the manuscript claim.
+ Remediation: Align cover-letter statements with the final manuscript disclosure text before export.
+- HIGH CLAIM_MISMATCH_ETHICS: Cover-letter ethics statement diverges from the manuscript claim.
+ Remediation: Align cover-letter statements with the final manuscript disclosure text before export.
+- MEDIUM CLAIM_MISMATCH_DATAAVAILABILITY: Cover-letter dataAvailability statement diverges from the manuscript claim.
+ Remediation: Align cover-letter statements with the final manuscript disclosure text before export.
+- HIGH CLAIM_MISMATCH_CONFLICTS: Cover-letter conflicts statement diverges from the manuscript claim.
+ Remediation: Align cover-letter statements with the final manuscript disclosure text before export.
+- HIGH PRIVATE_COMMENT_LEAK: Cover-letter export includes private reviewer or collaborator notes.
+ Remediation: Redact private comments and rebuild the editor-facing correspondence packet.
diff --git a/collab-cover-letter-export-guard/reports/summary.svg b/collab-cover-letter-export-guard/reports/summary.svg
new file mode 100644
index 00000000..4c71fe1d
--- /dev/null
+++ b/collab-cover-letter-export-guard/reports/summary.svg
@@ -0,0 +1,8 @@
+
diff --git a/collab-cover-letter-export-guard/src/guard.js b/collab-cover-letter-export-guard/src/guard.js
new file mode 100644
index 00000000..ba8ccc2d
--- /dev/null
+++ b/collab-cover-letter-export-guard/src/guard.js
@@ -0,0 +1,152 @@
+import crypto from "node:crypto";
+
+const CLAIM_KEYS = ["novelty", "ethics", "dataAvailability", "conflicts"];
+
+function normalizeText(value) {
+ return String(value || "")
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, " ")
+ .trim();
+}
+
+function textSimilarity(left, right) {
+ const leftTerms = new Set(normalizeText(left).split(" ").filter(Boolean));
+ const rightTerms = new Set(normalizeText(right).split(" ").filter(Boolean));
+ if (leftTerms.size === 0 && rightTerms.size === 0) return 1;
+ const overlap = [...leftTerms].filter((term) => rightTerms.has(term)).length;
+ return overlap / Math.max(leftTerms.size, rightTerms.size);
+}
+
+function addFinding(findings, severity, code, message, remediation) {
+ findings.push({ severity, code, message, remediation });
+}
+
+function hasPrivateLeak(packet) {
+ const noteText = packet.coverLetter.editorNotes.join("\n").toLowerCase();
+ return (
+ packet.coverLetter.privateCommentExports.length > 0 ||
+ /private reviewer|internal note|unresolved collaborator|do not disclose/.test(noteText)
+ );
+}
+
+function evaluateApprovalFreshness(packet, findings) {
+ const expectedHash = packet.manuscript.versionHash;
+ const cover = packet.coverLetter;
+
+ if (cover.manuscriptHash !== expectedHash) {
+ addFinding(
+ findings,
+ "high",
+ "STALE_COVER_LETTER_HASH",
+ `Cover letter references ${cover.manuscriptHash}, but final manuscript is ${expectedHash}.`,
+ "Regenerate the cover-letter packet from the final manuscript version.",
+ );
+ }
+
+ if (!cover.correspondingAuthorApproval || cover.correspondingAuthorApproval.approvedHash !== expectedHash) {
+ addFinding(
+ findings,
+ "high",
+ "MISSING_CORRESPONDING_AUTHOR_APPROVAL",
+ "Corresponding author approval is missing or tied to an older manuscript hash.",
+ "Collect a fresh corresponding-author approval after the final edit.",
+ );
+ }
+
+ const requiredCoauthors = packet.collaborators
+ .filter((collaborator) => collaborator.role === "coauthor")
+ .map((collaborator) => collaborator.authorId);
+ const approvedCoauthors = new Set(
+ cover.coauthorApprovals
+ .filter((approval) => approval.approvedHash === expectedHash)
+ .map((approval) => approval.authorId),
+ );
+ const missing = requiredCoauthors.filter((authorId) => !approvedCoauthors.has(authorId));
+ if (missing.length > 0) {
+ addFinding(
+ findings,
+ "medium",
+ "COAUTHOR_APPROVAL_DRIFT",
+ `Missing fresh cover-letter approval from ${missing.join(", ")}.`,
+ "Route the final cover-letter packet to every coauthor who is listed on the manuscript.",
+ );
+ }
+}
+
+function evaluateStatementConsistency(packet, findings) {
+ for (const key of CLAIM_KEYS) {
+ const manuscriptValue = packet.manuscript.claims[key];
+ const coverValue = packet.coverLetter.statements[key];
+ const similarity = textSimilarity(manuscriptValue, coverValue);
+ if (similarity < 0.45) {
+ addFinding(
+ findings,
+ key === "conflicts" || key === "ethics" ? "high" : "medium",
+ `CLAIM_MISMATCH_${key.toUpperCase()}`,
+ `Cover-letter ${key} statement diverges from the manuscript claim.`,
+ "Align cover-letter statements with the final manuscript disclosure text before export.",
+ );
+ }
+ }
+}
+
+export function evaluateCoverLetterExportPacket(packet) {
+ const findings = [];
+ evaluateApprovalFreshness(packet, findings);
+ evaluateStatementConsistency(packet, findings);
+
+ if (hasPrivateLeak(packet)) {
+ addFinding(
+ findings,
+ "high",
+ "PRIVATE_COMMENT_LEAK",
+ "Cover-letter export includes private reviewer or collaborator notes.",
+ "Redact private comments and rebuild the editor-facing correspondence packet.",
+ );
+ }
+
+ const highCount = findings.filter((finding) => finding.severity === "high").length;
+ const mediumCount = findings.filter((finding) => finding.severity === "medium").length;
+ const decision = highCount > 0 ? "HOLD" : mediumCount > 0 ? "REVIEW" : "RELEASE";
+ const auditDigest = crypto
+ .createHash("sha256")
+ .update(JSON.stringify({ id: packet.id, decision, findings }))
+ .digest("hex")
+ .slice(0, 16);
+
+ return {
+ packetId: packet.id,
+ decision,
+ findingCount: findings.length,
+ highCount,
+ mediumCount,
+ findings,
+ exportReady: decision === "RELEASE",
+ auditDigest,
+ };
+}
+
+export function renderMarkdownReport(packet, result) {
+ const lines = [
+ `# Cover Letter Export Guard Report`,
+ "",
+ `- Packet: ${packet.id}`,
+ `- Manuscript: ${packet.manuscript.title}`,
+ `- Decision: ${result.decision}`,
+ `- Audit digest: ${result.auditDigest}`,
+ "",
+ "## Findings",
+ "",
+ ];
+
+ if (result.findings.length === 0) {
+ lines.push("- No blockers found. Cover-letter export is ready for journal submission.");
+ } else {
+ for (const finding of result.findings) {
+ lines.push(`- ${finding.severity.toUpperCase()} ${finding.code}: ${finding.message}`);
+ lines.push(` Remediation: ${finding.remediation}`);
+ }
+ }
+
+ return `${lines.join("\n")}\n`;
+}
diff --git a/collab-cover-letter-export-guard/src/samplePackets.js b/collab-cover-letter-export-guard/src/samplePackets.js
new file mode 100644
index 00000000..ff1b5c42
--- /dev/null
+++ b/collab-cover-letter-export-guard/src/samplePackets.js
@@ -0,0 +1,91 @@
+export const cleanSubmissionPacket = {
+ id: "submission-ready-cover-letter",
+ manuscript: {
+ versionHash: "ms-v9-6f2a",
+ title: "Longitudinal biomarker stability in synthetic cohort data",
+ claims: {
+ novelty: "First preregistered multi-site synthetic stability analysis in this cohort.",
+ ethics: "Synthetic-only data with no human participant identifiers.",
+ dataAvailability: "Synthetic data and analysis code are available in the linked repository.",
+ conflicts: "The authors declare no competing interests.",
+ },
+ finalEditedAt: "2026-06-21T12:00:00Z",
+ },
+ coverLetter: {
+ manuscriptHash: "ms-v9-6f2a",
+ editedAt: "2026-06-21T12:08:00Z",
+ correspondingAuthorApproval: {
+ authorId: "author-1",
+ approvedHash: "ms-v9-6f2a",
+ approvedAt: "2026-06-21T12:12:00Z",
+ },
+ coauthorApprovals: [
+ { authorId: "author-2", approvedHash: "ms-v9-6f2a", approvedAt: "2026-06-21T12:20:00Z" },
+ { authorId: "author-3", approvedHash: "ms-v9-6f2a", approvedAt: "2026-06-21T12:21:00Z" },
+ ],
+ statements: {
+ novelty: "First preregistered multi-site synthetic stability analysis in this cohort.",
+ ethics: "Synthetic-only data with no human participant identifiers.",
+ dataAvailability: "Synthetic data and analysis code are available in the linked repository.",
+ conflicts: "The authors declare no competing interests.",
+ },
+ editorNotes: [
+ "We believe the study fits the journal scope because it demonstrates reproducible synthetic cohort validation.",
+ ],
+ privateCommentExports: [],
+ },
+ collaborators: [
+ { authorId: "author-1", role: "corresponding-author" },
+ { authorId: "author-2", role: "coauthor" },
+ { authorId: "author-3", role: "coauthor" },
+ ],
+};
+
+export const riskySubmissionPacket = {
+ id: "submission-blocked-cover-letter",
+ manuscript: {
+ versionHash: "ms-v11-aa30",
+ title: "Model-assisted detection of assay drift in synthetic submissions",
+ claims: {
+ novelty: "A synthetic benchmark for assay drift detection across three labs.",
+ ethics: "Synthetic samples only; no patient identifiers are included.",
+ dataAvailability: "Synthetic data will be released after editorial acceptance.",
+ conflicts: "Author B consults for LabScale Analytics.",
+ },
+ finalEditedAt: "2026-06-22T18:40:00Z",
+ },
+ coverLetter: {
+ manuscriptHash: "ms-v10-21bd",
+ editedAt: "2026-06-22T17:45:00Z",
+ correspondingAuthorApproval: {
+ authorId: "author-1",
+ approvedHash: "ms-v10-21bd",
+ approvedAt: "2026-06-22T17:50:00Z",
+ },
+ coauthorApprovals: [
+ { authorId: "author-2", approvedHash: "ms-v10-21bd", approvedAt: "2026-06-22T17:51:00Z" },
+ ],
+ statements: {
+ novelty: "The first assay drift detector validated in live clinical data.",
+ ethics: "No ethics statement is required.",
+ dataAvailability: "All raw data are publicly available now.",
+ conflicts: "The authors declare no competing interests.",
+ },
+ editorNotes: [
+ "Private reviewer note: Reviewer 2 questioned whether the synthetic cohort is sufficient.",
+ "Please consider this despite the unresolved collaborator comment in section 4.",
+ ],
+ privateCommentExports: [
+ {
+ commentId: "c-private-77",
+ section: "Results",
+ text: "Internal note: do not disclose LabScale consulting until legal confirms wording.",
+ },
+ ],
+ },
+ collaborators: [
+ { authorId: "author-1", role: "corresponding-author" },
+ { authorId: "author-2", role: "coauthor" },
+ { authorId: "author-3", role: "coauthor" },
+ ],
+};
diff --git a/collab-cover-letter-export-guard/test.js b/collab-cover-letter-export-guard/test.js
new file mode 100644
index 00000000..21b0d0f0
--- /dev/null
+++ b/collab-cover-letter-export-guard/test.js
@@ -0,0 +1,23 @@
+import assert from "node:assert/strict";
+import { cleanSubmissionPacket, riskySubmissionPacket } from "./src/samplePackets.js";
+import { evaluateCoverLetterExportPacket, renderMarkdownReport } from "./src/guard.js";
+
+const cleanResult = evaluateCoverLetterExportPacket(cleanSubmissionPacket);
+assert.equal(cleanResult.decision, "RELEASE");
+assert.equal(cleanResult.findingCount, 0);
+assert.equal(cleanResult.exportReady, true);
+
+const riskyResult = evaluateCoverLetterExportPacket(riskySubmissionPacket);
+assert.equal(riskyResult.decision, "HOLD");
+assert.equal(riskyResult.exportReady, false);
+assert.ok(riskyResult.findings.some((finding) => finding.code === "STALE_COVER_LETTER_HASH"));
+assert.ok(riskyResult.findings.some((finding) => finding.code === "MISSING_CORRESPONDING_AUTHOR_APPROVAL"));
+assert.ok(riskyResult.findings.some((finding) => finding.code === "PRIVATE_COMMENT_LEAK"));
+assert.ok(riskyResult.findings.some((finding) => finding.code === "CLAIM_MISMATCH_CONFLICTS"));
+
+const report = renderMarkdownReport(riskySubmissionPacket, riskyResult);
+assert.match(report, /Cover Letter Export Guard Report/);
+assert.match(report, /HOLD/);
+assert.match(report, /PRIVATE_COMMENT_LEAK/);
+
+console.log("4 cover-letter export guard tests passed");