diff --git a/collaborative-ethics-consent-guard/README.md b/collaborative-ethics-consent-guard/README.md
new file mode 100644
index 00000000..38838518
--- /dev/null
+++ b/collaborative-ethics-consent-guard/README.md
@@ -0,0 +1,57 @@
+# Collaborative Ethics and Consent Guard
+
+This module adds a focused collaborative ethics-and-consent export guard for the Real-Time Collaborative Editor in issue `#12`.
+
+Collaborative manuscripts can accumulate edits, comments, translations, and locked-section changes across many users. Before export, the editor needs a deterministic gate that checks whether ethics and consent content is safe to publish and whether private reviewer or participant details are still embedded in collaborative state.
+
+## Scope
+
+- Checks human-subjects manuscripts for ethics approval identifiers.
+- Flags expired approvals and missing exemption rationale.
+- Checks vulnerable-population review evidence.
+- Checks consent statement presence and waiver rationale.
+- Compares consent scope against the requested export scope.
+- Checks required translated-consent coverage.
+- Flags pending edits in locked ethics/consent sections without approved override.
+- Flags stale ethics-section review after collaborative edits.
+- Holds exports with private ethics comments or participant-like identifiers.
+- Produces JSON, Markdown, SVG, and MP4 reviewer artifacts.
+- Uses synthetic data only.
+- Uses no external APIs, private manuscripts, real participant data, credentials, payment systems, or network calls.
+
+## Requirement Map
+
+| Issue #12 capability | Implementation |
+| --- | --- |
+| Real-time collaboration | Reviews collaborative comments, pending edits, locks, and section state before export. |
+| Change tracking and suggestions | Detects pending ethics edits and unresolved ethics comments. |
+| Rich scientific manuscript export | Guards ethics/consent statements before manuscript export. |
+| Version history and autosave | Checks review freshness after collaborative changes. |
+| Inline comments and review sidebar | Blocks private reviewer notes and participant-like identifiers from public export. |
+| Publication-ready formatting | Confirms consent scope, translated-consent coverage, and ethics approval metadata. |
+
+## Run
+
+```bash
+npm test
+npm run demo
+npm run video
+npm run check
+```
+
+Demo artifacts are written to `reports/`:
+
+- `summary.json`
+- `reviewer-packet.md`
+- `summary.svg`
+- `demo.mp4`
+
+## Decisions
+
+- `RELEASE`: ethics, consent, comments, and export scope are publication-ready.
+- `REVIEW`: missing or stale evidence can be repaired before export.
+- `HOLD`: missing approval/consent, expired approval, private comment leakage, or participant identifiers block export.
+
+## AI Assistance Disclosure
+
+Implemented with Codex assistance and locally verified with deterministic tests and generated artifacts.
diff --git a/collaborative-ethics-consent-guard/data/sample_editor_packets.json b/collaborative-ethics-consent-guard/data/sample_editor_packets.json
new file mode 100644
index 00000000..6f81d174
--- /dev/null
+++ b/collaborative-ethics-consent-guard/data/sample_editor_packets.json
@@ -0,0 +1,162 @@
+[
+ {
+ "id": "release-clean-consent",
+ "title": "Clean collaborative consent export",
+ "reviewDate": "2026-08-15",
+ "manuscript": {
+ "domain": "public health",
+ "humanSubjects": true,
+ "participantGroups": ["adults"]
+ },
+ "ethics": {
+ "approvalId": "IRB-2026-114",
+ "expiryDate": "2027-01-31",
+ "vulnerablePopulationReview": false
+ },
+ "consent": {
+ "required": true,
+ "statementPresent": true,
+ "scope": "journal publication",
+ "requiredLanguages": ["English", "Spanish"],
+ "presentLanguages": ["English", "Spanish"]
+ },
+ "collaborativeState": {
+ "lastEthicsReviewAt": "2026-08-01",
+ "ethicsSection": {
+ "locked": true,
+ "pendingEdits": false
+ }
+ },
+ "comments": [
+ {
+ "id": "c1",
+ "section": "methods ethics",
+ "private": false,
+ "exportVisible": false,
+ "resolved": true,
+ "text": "Consent statement reviewed."
+ }
+ ],
+ "exportTarget": {
+ "releaseScope": "journal publication"
+ }
+ },
+ {
+ "id": "review-stale-vulnerable",
+ "title": "Vulnerable population review needs refresh",
+ "reviewDate": "2026-08-15",
+ "manuscript": {
+ "domain": "education research",
+ "humanSubjects": true,
+ "participantGroups": ["children"]
+ },
+ "ethics": {
+ "approvalId": "REC-2025-09",
+ "expiryDate": "2026-12-01",
+ "vulnerablePopulationReview": false
+ },
+ "consent": {
+ "required": true,
+ "statementPresent": true,
+ "scope": "conference abstract",
+ "requiredLanguages": ["English", "French"],
+ "presentLanguages": ["English"]
+ },
+ "collaborativeState": {
+ "lastEthicsReviewAt": "2025-11-01",
+ "ethicsSection": {
+ "locked": true,
+ "pendingEdits": true,
+ "approvedOverride": true
+ }
+ },
+ "comments": [
+ {
+ "id": "c2",
+ "section": "ethics statement",
+ "private": false,
+ "exportVisible": false,
+ "resolved": false,
+ "text": "Need French consent upload confirmation."
+ }
+ ],
+ "exportTarget": {
+ "releaseScope": "conference abstract"
+ }
+ },
+ {
+ "id": "hold-missing-approval",
+ "title": "Human-subjects packet without approval",
+ "reviewDate": "2026-08-15",
+ "manuscript": {
+ "domain": "clinical methods",
+ "humanSubjects": true,
+ "participantGroups": ["adults"]
+ },
+ "ethics": {
+ "approvalId": "",
+ "exempt": false
+ },
+ "consent": {
+ "required": true,
+ "statementPresent": false,
+ "scope": "journal publication",
+ "requiredLanguages": ["English"],
+ "presentLanguages": []
+ },
+ "collaborativeState": {
+ "lastEthicsReviewAt": "2026-08-01",
+ "ethicsSection": {
+ "locked": true,
+ "pendingEdits": false
+ }
+ },
+ "comments": [],
+ "exportTarget": {
+ "releaseScope": "journal publication"
+ }
+ },
+ {
+ "id": "hold-private-comment-leak",
+ "title": "Private participant details leaking through comments",
+ "reviewDate": "2026-08-15",
+ "manuscript": {
+ "domain": "behavioral science",
+ "humanSubjects": true,
+ "participantGroups": ["adults"]
+ },
+ "ethics": {
+ "approvalId": "IRB-2026-202",
+ "expiryDate": "2027-03-01",
+ "vulnerablePopulationReview": false
+ },
+ "consent": {
+ "required": true,
+ "statementPresent": true,
+ "scope": "journal publication",
+ "requiredLanguages": ["English"],
+ "presentLanguages": ["English"]
+ },
+ "collaborativeState": {
+ "lastEthicsReviewAt": "2026-07-20",
+ "ethicsSection": {
+ "locked": true,
+ "pendingEdits": true,
+ "approvedOverride": false
+ }
+ },
+ "comments": [
+ {
+ "id": "c3",
+ "section": "ethics statement",
+ "private": true,
+ "exportVisible": true,
+ "resolved": false,
+ "text": "participant 104 withdrew after DOB: 02/04/1999 note."
+ }
+ ],
+ "exportTarget": {
+ "releaseScope": "journal publication"
+ }
+ }
+]
diff --git a/collaborative-ethics-consent-guard/package.json b/collaborative-ethics-consent-guard/package.json
new file mode 100644
index 00000000..be486878
--- /dev/null
+++ b/collaborative-ethics-consent-guard/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "collaborative-ethics-consent-guard",
+ "version": "1.0.0",
+ "description": "Dependency-free ethics and consent export guard for collaborative research manuscripts.",
+ "main": "src/index.js",
+ "scripts": {
+ "test": "node --test",
+ "demo": "node scripts/demo.js",
+ "video": "node scripts/render-demo-video.js",
+ "check": "node --check src/index.js && node --check scripts/demo.js && node --check scripts/render-demo-video.js"
+ },
+ "license": "MIT"
+}
diff --git a/collaborative-ethics-consent-guard/reports/demo.mp4 b/collaborative-ethics-consent-guard/reports/demo.mp4
new file mode 100644
index 00000000..2818b1b3
Binary files /dev/null and b/collaborative-ethics-consent-guard/reports/demo.mp4 differ
diff --git a/collaborative-ethics-consent-guard/reports/reviewer-packet.md b/collaborative-ethics-consent-guard/reports/reviewer-packet.md
new file mode 100644
index 00000000..d4307b73
--- /dev/null
+++ b/collaborative-ethics-consent-guard/reports/reviewer-packet.md
@@ -0,0 +1,62 @@
+# Collaborative Ethics and Consent Export Review
+
+Generated: 2026-08-15T21:08:58.890Z
+
+## Decision Counts
+
+- RELEASE: 1
+- REVIEW: 1
+- HOLD: 2
+
+## Packet Reviews
+
+### Clean collaborative consent export
+Decision: RELEASE
+Confidence: 0.96
+Human subjects: true
+Comments checked: 1
+Reviewer note: public health: ethics, consent, comments, and export scope are publication-ready.
+
+### Vulnerable population review needs refresh
+Decision: REVIEW
+Confidence: 0.31
+Human subjects: true
+Comments checked: 1
+Reviewer note: education research: REVIEW before export because the manuscript includes vulnerable participant groups without documented enhanced ethics review.
+- MAJOR VULNERABLE_POPULATION_REVIEW_MISSING: The manuscript includes vulnerable participant groups without documented enhanced ethics review.
+ Remediation: Add vulnerable-population review evidence and consent safeguards before publication export.
+- MAJOR TRANSLATED_CONSENT_COVERAGE_MISSING: Required consent-language coverage is missing for one or more participant groups.
+ Remediation: Add translated consent evidence or justify why those participant groups are not included in the export.
+- MAJOR STALE_ETHICS_REVIEW: The last ethics-section review is older than the configured freshness window.
+ Remediation: Refresh ethics reviewer signoff after the latest collaborative edits.
+- MINOR UNRESOLVED_ETHICS_COMMENT: An ethics-section comment remains unresolved at export time.
+ Remediation: Resolve or explicitly defer the ethics comment before publication export.
+
+### Human-subjects packet without approval
+Decision: HOLD
+Confidence: 0.11
+Human subjects: true
+Comments checked: 0
+Reviewer note: clinical methods: HOLD before export because the manuscript involves human subjects but lacks an ethics approval identifier.
+- CRITICAL MISSING_ETHICS_APPROVAL_ID: The manuscript involves human subjects but lacks an ethics approval identifier.
+ Remediation: Add the IRB/REC approval identifier or mark the study as exempt with supporting rationale.
+- CRITICAL MISSING_CONSENT_STATEMENT: Consent is required but no consent statement is present in the collaborative manuscript.
+ Remediation: Add a consent statement or documented waiver before export.
+- MAJOR TRANSLATED_CONSENT_COVERAGE_MISSING: Required consent-language coverage is missing for one or more participant groups.
+ Remediation: Add translated consent evidence or justify why those participant groups are not included in the export.
+
+### Private participant details leaking through comments
+Decision: HOLD
+Confidence: 0.1
+Human subjects: true
+Comments checked: 1
+Reviewer note: behavioral science: HOLD before export because a locked ethics or consent section has pending collaborative edits without approved override.
+- MAJOR LOCKED_ETHICS_SECTION_HAS_PENDING_EDITS: A locked ethics or consent section has pending collaborative edits without approved override.
+ Remediation: Route pending edits through the section owner or ethics reviewer before export.
+- CRITICAL PRIVATE_ETHICS_COMMENT_VISIBLE_IN_EXPORT: A private ethics or reviewer comment is marked visible in the export packet.
+ Remediation: Redact or remove private comments before creating the public manuscript export.
+- CRITICAL PARTICIPANT_IDENTIFIER_IN_COMMENT: A collaborative comment appears to contain a participant, subject, patient, MRN, or DOB marker.
+ Remediation: Redact participant identifiers and regenerate reviewer-safe comments before export.
+- MINOR UNRESOLVED_ETHICS_COMMENT: An ethics-section comment remains unresolved at export time.
+ Remediation: Resolve or explicitly defer the ethics comment before publication export.
+
diff --git a/collaborative-ethics-consent-guard/reports/summary.json b/collaborative-ethics-consent-guard/reports/summary.json
new file mode 100644
index 00000000..502db089
--- /dev/null
+++ b/collaborative-ethics-consent-guard/reports/summary.json
@@ -0,0 +1,169 @@
+{
+ "generatedAt": "2026-08-15T21:08:58.890Z",
+ "packetCount": 4,
+ "decisionCounts": {
+ "RELEASE": 1,
+ "REVIEW": 1,
+ "HOLD": 2
+ },
+ "reviews": [
+ {
+ "id": "release-clean-consent",
+ "title": "Clean collaborative consent export",
+ "decision": "RELEASE",
+ "confidence": 0.96,
+ "humanSubjects": true,
+ "commentCount": 1,
+ "findingCount": 0,
+ "findings": [],
+ "reviewerNote": "public health: ethics, consent, comments, and export scope are publication-ready."
+ },
+ {
+ "id": "review-stale-vulnerable",
+ "title": "Vulnerable population review needs refresh",
+ "decision": "REVIEW",
+ "confidence": 0.31,
+ "humanSubjects": true,
+ "commentCount": 1,
+ "findingCount": 4,
+ "findings": [
+ {
+ "code": "VULNERABLE_POPULATION_REVIEW_MISSING",
+ "severity": "major",
+ "message": "The manuscript includes vulnerable participant groups without documented enhanced ethics review.",
+ "evidence": {
+ "vulnerableGroups": [
+ "children"
+ ]
+ },
+ "remediation": "Add vulnerable-population review evidence and consent safeguards before publication export."
+ },
+ {
+ "code": "TRANSLATED_CONSENT_COVERAGE_MISSING",
+ "severity": "major",
+ "message": "Required consent-language coverage is missing for one or more participant groups.",
+ "evidence": {
+ "missingLanguages": [
+ "french"
+ ]
+ },
+ "remediation": "Add translated consent evidence or justify why those participant groups are not included in the export."
+ },
+ {
+ "code": "STALE_ETHICS_REVIEW",
+ "severity": "major",
+ "message": "The last ethics-section review is older than the configured freshness window.",
+ "evidence": {
+ "lastEthicsReviewAt": "2025-11-01",
+ "maxAgeDays": 180
+ },
+ "remediation": "Refresh ethics reviewer signoff after the latest collaborative edits."
+ },
+ {
+ "code": "UNRESOLVED_ETHICS_COMMENT",
+ "severity": "minor",
+ "message": "An ethics-section comment remains unresolved at export time.",
+ "evidence": {
+ "commentId": "c2"
+ },
+ "remediation": "Resolve or explicitly defer the ethics comment before publication export."
+ }
+ ],
+ "reviewerNote": "education research: REVIEW before export because the manuscript includes vulnerable participant groups without documented enhanced ethics review."
+ },
+ {
+ "id": "hold-missing-approval",
+ "title": "Human-subjects packet without approval",
+ "decision": "HOLD",
+ "confidence": 0.11,
+ "humanSubjects": true,
+ "commentCount": 0,
+ "findingCount": 3,
+ "findings": [
+ {
+ "code": "MISSING_ETHICS_APPROVAL_ID",
+ "severity": "critical",
+ "message": "The manuscript involves human subjects but lacks an ethics approval identifier.",
+ "evidence": {
+ "humanSubjects": true,
+ "approvalId": null
+ },
+ "remediation": "Add the IRB/REC approval identifier or mark the study as exempt with supporting rationale."
+ },
+ {
+ "code": "MISSING_CONSENT_STATEMENT",
+ "severity": "critical",
+ "message": "Consent is required but no consent statement is present in the collaborative manuscript.",
+ "evidence": {
+ "consentRequired": true,
+ "statementPresent": false
+ },
+ "remediation": "Add a consent statement or documented waiver before export."
+ },
+ {
+ "code": "TRANSLATED_CONSENT_COVERAGE_MISSING",
+ "severity": "major",
+ "message": "Required consent-language coverage is missing for one or more participant groups.",
+ "evidence": {
+ "missingLanguages": [
+ "english"
+ ]
+ },
+ "remediation": "Add translated consent evidence or justify why those participant groups are not included in the export."
+ }
+ ],
+ "reviewerNote": "clinical methods: HOLD before export because the manuscript involves human subjects but lacks an ethics approval identifier."
+ },
+ {
+ "id": "hold-private-comment-leak",
+ "title": "Private participant details leaking through comments",
+ "decision": "HOLD",
+ "confidence": 0.1,
+ "humanSubjects": true,
+ "commentCount": 1,
+ "findingCount": 4,
+ "findings": [
+ {
+ "code": "LOCKED_ETHICS_SECTION_HAS_PENDING_EDITS",
+ "severity": "major",
+ "message": "A locked ethics or consent section has pending collaborative edits without approved override.",
+ "evidence": {
+ "locked": true,
+ "pendingEdits": true
+ },
+ "remediation": "Route pending edits through the section owner or ethics reviewer before export."
+ },
+ {
+ "code": "PRIVATE_ETHICS_COMMENT_VISIBLE_IN_EXPORT",
+ "severity": "critical",
+ "message": "A private ethics or reviewer comment is marked visible in the export packet.",
+ "evidence": {
+ "commentId": "c3",
+ "section": "ethics statement"
+ },
+ "remediation": "Redact or remove private comments before creating the public manuscript export."
+ },
+ {
+ "code": "PARTICIPANT_IDENTIFIER_IN_COMMENT",
+ "severity": "critical",
+ "message": "A collaborative comment appears to contain a participant, subject, patient, MRN, or DOB marker.",
+ "evidence": {
+ "commentId": "c3",
+ "section": "ethics statement"
+ },
+ "remediation": "Redact participant identifiers and regenerate reviewer-safe comments before export."
+ },
+ {
+ "code": "UNRESOLVED_ETHICS_COMMENT",
+ "severity": "minor",
+ "message": "An ethics-section comment remains unresolved at export time.",
+ "evidence": {
+ "commentId": "c3"
+ },
+ "remediation": "Resolve or explicitly defer the ethics comment before publication export."
+ }
+ ],
+ "reviewerNote": "behavioral science: HOLD before export because a locked ethics or consent section has pending collaborative edits without approved override."
+ }
+ ]
+}
diff --git a/collaborative-ethics-consent-guard/reports/summary.svg b/collaborative-ethics-consent-guard/reports/summary.svg
new file mode 100644
index 00000000..c46e4698
--- /dev/null
+++ b/collaborative-ethics-consent-guard/reports/summary.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/collaborative-ethics-consent-guard/scripts/demo.js b/collaborative-ethics-consent-guard/scripts/demo.js
new file mode 100644
index 00000000..e8da0c0d
--- /dev/null
+++ b/collaborative-ethics-consent-guard/scripts/demo.js
@@ -0,0 +1,76 @@
+"use strict";
+
+const fs = require("node:fs");
+const path = require("node:path");
+const { analyzePackets } = require("../src");
+
+const root = path.resolve(__dirname, "..");
+const packets = JSON.parse(fs.readFileSync(path.join(root, "data", "sample_editor_packets.json"), "utf8"));
+const report = analyzePackets(packets);
+const outDir = path.join(root, "reports");
+
+fs.mkdirSync(outDir, { recursive: true });
+fs.writeFileSync(path.join(outDir, "summary.json"), `${JSON.stringify(report, null, 2)}\n`);
+fs.writeFileSync(path.join(outDir, "reviewer-packet.md"), renderMarkdown(report));
+fs.writeFileSync(path.join(outDir, "summary.svg"), renderSvg(report));
+
+console.log(JSON.stringify({
+ packetCount: report.packetCount,
+ decisionCounts: report.decisionCounts,
+ reports: [
+ "reports/summary.json",
+ "reports/reviewer-packet.md",
+ "reports/summary.svg"
+ ]
+}, null, 2));
+
+function renderMarkdown(report) {
+ const lines = [
+ "# Collaborative Ethics and Consent Export Review",
+ "",
+ `Generated: ${report.generatedAt}`,
+ "",
+ "## Decision Counts",
+ "",
+ `- RELEASE: ${report.decisionCounts.RELEASE}`,
+ `- REVIEW: ${report.decisionCounts.REVIEW}`,
+ `- HOLD: ${report.decisionCounts.HOLD}`,
+ "",
+ "## Packet Reviews",
+ ""
+ ];
+
+ for (const review of report.reviews) {
+ lines.push(`### ${review.title}`);
+ lines.push(`Decision: ${review.decision}`);
+ lines.push(`Confidence: ${review.confidence}`);
+ lines.push(`Human subjects: ${review.humanSubjects}`);
+ lines.push(`Comments checked: ${review.commentCount}`);
+ lines.push(`Reviewer note: ${review.reviewerNote}`);
+ for (const item of review.findings) {
+ lines.push(`- ${item.severity.toUpperCase()} ${item.code}: ${item.message}`);
+ lines.push(` Remediation: ${item.remediation}`);
+ }
+ lines.push("");
+ }
+
+ return `${lines.join("\n")}\n`;
+}
+
+function renderSvg(report) {
+ return ``;
+}
+
+function bar(label, count, x, y, color) {
+ const width = 96 + count * 116;
+ return `${label}: ${count}
+
+ `;
+}
diff --git a/collaborative-ethics-consent-guard/scripts/render-demo-video.js b/collaborative-ethics-consent-guard/scripts/render-demo-video.js
new file mode 100644
index 00000000..3e01f6c5
--- /dev/null
+++ b/collaborative-ethics-consent-guard/scripts/render-demo-video.js
@@ -0,0 +1,60 @@
+"use strict";
+
+const fs = require("node:fs");
+const path = require("node:path");
+const { spawnSync } = require("node:child_process");
+
+const root = path.resolve(__dirname, "..");
+const ffmpeg = path.resolve(
+ root,
+ "..",
+ "..",
+ "tool_downloads",
+ "video_tools",
+ "node_modules",
+ "ffmpeg-static",
+ "ffmpeg.exe"
+);
+const outDir = path.join(root, "reports");
+const out = path.join(outDir, "demo.mp4");
+const font = "C\\:/Windows/Fonts/arial.ttf";
+
+fs.mkdirSync(outDir, { recursive: true });
+
+const draw = [
+ `drawtext=fontfile=${font}:text='Collaborative Ethics and Consent':x=64:y=56:fontsize=42:fontcolor=black`,
+ `drawtext=fontfile=${font}:text='Real-time editor export guard for human-subjects manuscripts':x=64:y=120:fontsize=25:fontcolor=0x34413b`,
+ `drawtext=fontfile=${font}:text='Synthetic editor packets analyzed 4':x=64:y=210:fontsize=34:fontcolor=black`,
+ `drawtext=fontfile=${font}:text='RELEASE 1 REVIEW 1 HOLD 2':x=64:y=268:fontsize=38:fontcolor=0x245fd6`,
+ `drawtext=fontfile=${font}:text='Checks approval IDs consent scope translations locked ethics edits':x=64:y=358:fontsize=26:fontcolor=0x34413b`,
+ `drawtext=fontfile=${font}:text='Flags participant identifiers private comments and expired approvals':x=64:y=422:fontsize=26:fontcolor=0x34413b`,
+ `drawtext=fontfile=${font}:text='Synthetic generated slate only no desktop capture or private data':x=64:y=486:fontsize=26:fontcolor=0x34413b`
+].join(",");
+
+if (!fs.existsSync(ffmpeg)) {
+ throw new Error(`ffmpeg binary not found at ${ffmpeg}`);
+}
+
+const result = spawnSync(ffmpeg, [
+ "-y",
+ "-f",
+ "lavfi",
+ "-i",
+ "color=c=0xf9faf8:s=1280x720:d=8:r=30",
+ "-vf",
+ draw,
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-movflags",
+ "+faststart",
+ out
+], { encoding: "utf8" });
+
+if (result.status !== 0) {
+ process.stderr.write(result.stderr || result.stdout);
+ process.exit(result.status || 1);
+}
+
+console.log(JSON.stringify({ out, bytes: fs.statSync(out).size }, null, 2));
diff --git a/collaborative-ethics-consent-guard/src/index.js b/collaborative-ethics-consent-guard/src/index.js
new file mode 100644
index 00000000..bbc17263
--- /dev/null
+++ b/collaborative-ethics-consent-guard/src/index.js
@@ -0,0 +1,297 @@
+"use strict";
+
+const PARTICIPANT_MARKERS = [
+ /\bparticipant\s+\d{2,}\b/i,
+ /\bsubject\s+\d{2,}\b/i,
+ /\bpatient\s+\d{2,}\b/i,
+ /\bmrn[:\s-]*\d{4,}\b/i,
+ /\bdob[:\s-]*\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/i
+];
+
+const VULNERABLE_GROUPS = new Set([
+ "children",
+ "minors",
+ "incarcerated people",
+ "pregnant participants",
+ "cognitively impaired participants",
+ "indigenous communities"
+]);
+
+function finding(code, severity, message, evidence, remediation) {
+ return { code, severity, message, evidence, remediation };
+}
+
+function normalizeText(value) {
+ return String(value || "").trim().toLowerCase();
+}
+
+function list(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function hasParticipantMarker(value) {
+ const text = String(value || "");
+ return PARTICIPANT_MARKERS.some((pattern) => pattern.test(text));
+}
+
+function parseDate(value) {
+ const time = Date.parse(value);
+ return Number.isFinite(time) ? new Date(time) : null;
+}
+
+function isOlderThanDays(dateValue, referenceValue, maxAgeDays) {
+ const date = parseDate(dateValue);
+ const reference = parseDate(referenceValue);
+ if (!date || !reference) return false;
+ return (reference.getTime() - date.getTime()) / 86400000 > maxAgeDays;
+}
+
+function normalizePacket(packet) {
+ return {
+ id: packet.id,
+ title: packet.title || "Untitled collaborative manuscript packet",
+ reviewDate: packet.reviewDate || "2026-08-15",
+ manuscript: packet.manuscript || {},
+ ethics: packet.ethics || {},
+ consent: packet.consent || {},
+ collaborativeState: packet.collaborativeState || {},
+ comments: list(packet.comments),
+ exportTarget: packet.exportTarget || {}
+ };
+}
+
+function analyzeEthics(packet) {
+ const findings = [];
+ const ethics = packet.ethics;
+ const manuscript = packet.manuscript;
+
+ if (manuscript.humanSubjects === true && !ethics.approvalId) {
+ findings.push(finding(
+ "MISSING_ETHICS_APPROVAL_ID",
+ "critical",
+ "The manuscript involves human subjects but lacks an ethics approval identifier.",
+ { humanSubjects: manuscript.humanSubjects, approvalId: ethics.approvalId || null },
+ "Add the IRB/REC approval identifier or mark the study as exempt with supporting rationale."
+ ));
+ }
+
+ if (ethics.exempt === true && !ethics.exemptionRationale) {
+ findings.push(finding(
+ "MISSING_EXEMPTION_RATIONALE",
+ "major",
+ "The packet marks the study exempt but does not include an exemption rationale.",
+ { exempt: ethics.exempt },
+ "Add the exemption category, approving body, and rationale before export."
+ ));
+ }
+
+ if (ethics.approvalId && ethics.expiryDate && parseDate(ethics.expiryDate) && parseDate(ethics.expiryDate) < parseDate(packet.reviewDate)) {
+ findings.push(finding(
+ "ETHICS_APPROVAL_EXPIRED",
+ "critical",
+ "The ethics approval appears expired before the export review date.",
+ { approvalId: ethics.approvalId, expiryDate: ethics.expiryDate, reviewDate: packet.reviewDate },
+ "Hold export until renewed approval or a no-further-review determination is documented."
+ ));
+ }
+
+ const participantGroups = list(manuscript.participantGroups).map(normalizeText);
+ const vulnerable = participantGroups.filter((group) => VULNERABLE_GROUPS.has(group));
+ if (vulnerable.length > 0 && ethics.vulnerablePopulationReview !== true) {
+ findings.push(finding(
+ "VULNERABLE_POPULATION_REVIEW_MISSING",
+ "major",
+ "The manuscript includes vulnerable participant groups without documented enhanced ethics review.",
+ { vulnerableGroups: vulnerable },
+ "Add vulnerable-population review evidence and consent safeguards before publication export."
+ ));
+ }
+
+ return findings;
+}
+
+function analyzeConsent(packet) {
+ const findings = [];
+ const consent = packet.consent;
+ const manuscript = packet.manuscript;
+ const target = packet.exportTarget;
+
+ if (manuscript.humanSubjects === true && consent.required === true && consent.statementPresent !== true) {
+ findings.push(finding(
+ "MISSING_CONSENT_STATEMENT",
+ "critical",
+ "Consent is required but no consent statement is present in the collaborative manuscript.",
+ { consentRequired: consent.required, statementPresent: consent.statementPresent || false },
+ "Add a consent statement or documented waiver before export."
+ ));
+ }
+
+ if (consent.required === false && !consent.waiverReason && manuscript.humanSubjects === true) {
+ findings.push(finding(
+ "CONSENT_WAIVER_REASON_MISSING",
+ "major",
+ "The packet says consent is not required but provides no waiver reason.",
+ { consentRequired: consent.required, waiverReason: consent.waiverReason || null },
+ "Add the consent waiver rationale and approving body."
+ ));
+ }
+
+ if (consent.statementPresent === true && consent.scope && target.releaseScope && normalizeText(consent.scope) !== normalizeText(target.releaseScope)) {
+ findings.push(finding(
+ "CONSENT_SCOPE_EXPORT_MISMATCH",
+ "major",
+ "The consent statement scope does not match the requested export scope.",
+ { consentScope: consent.scope, exportScope: target.releaseScope },
+ "Update the export target or document consent coverage for the publication scope."
+ ));
+ }
+
+ const requiredLanguages = list(consent.requiredLanguages).map(normalizeText);
+ const presentLanguages = list(consent.presentLanguages).map(normalizeText);
+ const missingLanguages = requiredLanguages.filter((language) => !presentLanguages.includes(language));
+ if (missingLanguages.length > 0) {
+ findings.push(finding(
+ "TRANSLATED_CONSENT_COVERAGE_MISSING",
+ "major",
+ "Required consent-language coverage is missing for one or more participant groups.",
+ { missingLanguages },
+ "Add translated consent evidence or justify why those participant groups are not included in the export."
+ ));
+ }
+
+ return findings;
+}
+
+function analyzeCollaboration(packet) {
+ const findings = [];
+ const state = packet.collaborativeState;
+ const ethicsSection = state.ethicsSection || {};
+
+ if (ethicsSection.locked === true && ethicsSection.pendingEdits === true && ethicsSection.approvedOverride !== true) {
+ findings.push(finding(
+ "LOCKED_ETHICS_SECTION_HAS_PENDING_EDITS",
+ "major",
+ "A locked ethics or consent section has pending collaborative edits without approved override.",
+ { locked: ethicsSection.locked, pendingEdits: ethicsSection.pendingEdits },
+ "Route pending edits through the section owner or ethics reviewer before export."
+ ));
+ }
+
+ if (state.lastEthicsReviewAt && isOlderThanDays(state.lastEthicsReviewAt, packet.reviewDate, 180)) {
+ findings.push(finding(
+ "STALE_ETHICS_REVIEW",
+ "major",
+ "The last ethics-section review is older than the configured freshness window.",
+ { lastEthicsReviewAt: state.lastEthicsReviewAt, maxAgeDays: 180 },
+ "Refresh ethics reviewer signoff after the latest collaborative edits."
+ ));
+ }
+
+ for (const comment of packet.comments) {
+ if (comment.private === true && comment.exportVisible === true) {
+ findings.push(finding(
+ "PRIVATE_ETHICS_COMMENT_VISIBLE_IN_EXPORT",
+ "critical",
+ "A private ethics or reviewer comment is marked visible in the export packet.",
+ { commentId: comment.id || null, section: comment.section || null },
+ "Redact or remove private comments before creating the public manuscript export."
+ ));
+ }
+
+ if (hasParticipantMarker(comment.text)) {
+ findings.push(finding(
+ "PARTICIPANT_IDENTIFIER_IN_COMMENT",
+ "critical",
+ "A collaborative comment appears to contain a participant, subject, patient, MRN, or DOB marker.",
+ { commentId: comment.id || null, section: comment.section || null },
+ "Redact participant identifiers and regenerate reviewer-safe comments before export."
+ ));
+ }
+
+ if (comment.resolved !== true && normalizeText(comment.section).includes("ethics")) {
+ findings.push(finding(
+ "UNRESOLVED_ETHICS_COMMENT",
+ "minor",
+ "An ethics-section comment remains unresolved at export time.",
+ { commentId: comment.id || null },
+ "Resolve or explicitly defer the ethics comment before publication export."
+ ));
+ }
+ }
+
+ return findings;
+}
+
+function severityWeight(severity) {
+ if (severity === "critical") return 5;
+ if (severity === "major") return 3;
+ return 1;
+}
+
+function decisionFromFindings(findings) {
+ if (findings.some((item) => item.severity === "critical")) return "HOLD";
+ if (findings.length > 0) return "REVIEW";
+ return "RELEASE";
+}
+
+function confidenceFromFindings(findings) {
+ const penalty = findings.reduce((total, item) => total + severityWeight(item.severity), 0);
+ return Math.max(0.1, Number((0.96 - penalty * 0.065).toFixed(2)));
+}
+
+function reviewerNote(packet, decision, findings) {
+ if (decision === "RELEASE") {
+ return `${packet.manuscript.domain || "collaborative manuscript"}: ethics, consent, comments, and export scope are publication-ready.`;
+ }
+ const first = findings[0];
+ return `${packet.manuscript.domain || "collaborative manuscript"}: ${decision} before export because ${first.message.toLowerCase()}`;
+}
+
+function analyzePacket(input) {
+ const packet = normalizePacket(input);
+ const findings = [
+ ...analyzeEthics(packet),
+ ...analyzeConsent(packet),
+ ...analyzeCollaboration(packet)
+ ];
+ const decision = decisionFromFindings(findings);
+
+ return {
+ id: packet.id,
+ title: packet.title,
+ decision,
+ confidence: confidenceFromFindings(findings),
+ humanSubjects: packet.manuscript.humanSubjects === true,
+ commentCount: packet.comments.length,
+ findingCount: findings.length,
+ findings,
+ reviewerNote: reviewerNote(packet, decision, findings)
+ };
+}
+
+function analyzePackets(packets) {
+ const reviews = packets.map(analyzePacket);
+ const counts = reviews.reduce((acc, review) => {
+ acc[review.decision] = (acc[review.decision] || 0) + 1;
+ return acc;
+ }, {});
+
+ return {
+ generatedAt: new Date().toISOString(),
+ packetCount: reviews.length,
+ decisionCounts: {
+ RELEASE: counts.RELEASE || 0,
+ REVIEW: counts.REVIEW || 0,
+ HOLD: counts.HOLD || 0
+ },
+ reviews
+ };
+}
+
+module.exports = {
+ PARTICIPANT_MARKERS,
+ analyzePacket,
+ analyzePackets,
+ decisionFromFindings,
+ hasParticipantMarker
+};
diff --git a/collaborative-ethics-consent-guard/test/collaborative-ethics-consent-guard.test.js b/collaborative-ethics-consent-guard/test/collaborative-ethics-consent-guard.test.js
new file mode 100644
index 00000000..31418fa2
--- /dev/null
+++ b/collaborative-ethics-consent-guard/test/collaborative-ethics-consent-guard.test.js
@@ -0,0 +1,148 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const test = require("node:test");
+const { analyzePacket, analyzePackets, decisionFromFindings, hasParticipantMarker } = require("../src");
+
+test("detects participant-like identifiers in collaborative comments", () => {
+ assert.equal(hasParticipantMarker("participant 104 withdrew"), true);
+ assert.equal(hasParticipantMarker("ethics wording reviewed"), false);
+});
+
+test("releases complete ethics and consent export packets", () => {
+ const review = analyzePacket({
+ id: "clean",
+ title: "Clean packet",
+ reviewDate: "2026-08-15",
+ manuscript: { humanSubjects: true, participantGroups: ["adults"], domain: "public health" },
+ ethics: { approvalId: "IRB-1", expiryDate: "2027-01-01" },
+ consent: {
+ required: true,
+ statementPresent: true,
+ scope: "journal publication",
+ requiredLanguages: ["English"],
+ presentLanguages: ["English"]
+ },
+ collaborativeState: {
+ lastEthicsReviewAt: "2026-08-01",
+ ethicsSection: { locked: true, pendingEdits: false }
+ },
+ comments: [{ section: "ethics", resolved: true, text: "ready" }],
+ exportTarget: { releaseScope: "journal publication" }
+ });
+
+ assert.equal(review.decision, "RELEASE");
+ assert.equal(review.findingCount, 0);
+});
+
+test("holds human-subjects manuscript without approval or consent", () => {
+ const review = analyzePacket({
+ id: "missing",
+ reviewDate: "2026-08-15",
+ manuscript: { humanSubjects: true, participantGroups: ["adults"] },
+ ethics: {},
+ consent: { required: true, statementPresent: false },
+ collaborativeState: {},
+ comments: [],
+ exportTarget: {}
+ });
+
+ assert.equal(review.decision, "HOLD");
+ assert.ok(review.findings.some((item) => item.code === "MISSING_ETHICS_APPROVAL_ID"));
+ assert.ok(review.findings.some((item) => item.code === "MISSING_CONSENT_STATEMENT"));
+});
+
+test("routes vulnerable population and stale review gaps to review", () => {
+ const review = analyzePacket({
+ id: "review",
+ reviewDate: "2026-08-15",
+ manuscript: { humanSubjects: true, participantGroups: ["children"] },
+ ethics: { approvalId: "REC-1", expiryDate: "2027-01-01", vulnerablePopulationReview: false },
+ consent: {
+ required: true,
+ statementPresent: true,
+ scope: "conference abstract",
+ requiredLanguages: ["English", "French"],
+ presentLanguages: ["English"]
+ },
+ collaborativeState: {
+ lastEthicsReviewAt: "2025-11-01",
+ ethicsSection: { locked: true, pendingEdits: false }
+ },
+ comments: [{ id: "c1", section: "ethics", resolved: false, text: "Need language proof" }],
+ exportTarget: { releaseScope: "conference abstract" }
+ });
+
+ assert.equal(review.decision, "REVIEW");
+ assert.ok(review.findings.some((item) => item.code === "VULNERABLE_POPULATION_REVIEW_MISSING"));
+ assert.ok(review.findings.some((item) => item.code === "TRANSLATED_CONSENT_COVERAGE_MISSING"));
+ assert.ok(review.findings.some((item) => item.code === "STALE_ETHICS_REVIEW"));
+});
+
+test("holds private comments and participant identifiers visible in export", () => {
+ const review = analyzePacket({
+ id: "private",
+ reviewDate: "2026-08-15",
+ manuscript: { humanSubjects: true, participantGroups: ["adults"] },
+ ethics: { approvalId: "IRB-2", expiryDate: "2027-01-01" },
+ consent: {
+ required: true,
+ statementPresent: true,
+ scope: "journal publication",
+ requiredLanguages: ["English"],
+ presentLanguages: ["English"]
+ },
+ collaborativeState: {
+ lastEthicsReviewAt: "2026-08-01",
+ ethicsSection: { locked: true, pendingEdits: true, approvedOverride: false }
+ },
+ comments: [
+ {
+ id: "c2",
+ section: "ethics",
+ private: true,
+ exportVisible: true,
+ resolved: false,
+ text: "participant 104 has DOB: 02/04/1999"
+ }
+ ],
+ exportTarget: { releaseScope: "journal publication" }
+ });
+
+ assert.equal(review.decision, "HOLD");
+ assert.ok(review.findings.some((item) => item.code === "PRIVATE_ETHICS_COMMENT_VISIBLE_IN_EXPORT"));
+ assert.ok(review.findings.some((item) => item.code === "PARTICIPANT_IDENTIFIER_IN_COMMENT"));
+});
+
+test("summarizes packet decisions deterministically", () => {
+ const report = analyzePackets([
+ {
+ id: "release",
+ manuscript: { humanSubjects: false },
+ ethics: {},
+ consent: {},
+ collaborativeState: {},
+ comments: [],
+ exportTarget: {}
+ },
+ {
+ id: "hold",
+ manuscript: { humanSubjects: true },
+ ethics: {},
+ consent: { required: true, statementPresent: false },
+ collaborativeState: {},
+ comments: [],
+ exportTarget: {}
+ }
+ ]);
+
+ assert.equal(report.packetCount, 2);
+ assert.equal(report.decisionCounts.RELEASE, 1);
+ assert.equal(report.decisionCounts.HOLD, 1);
+});
+
+test("decision helper maps severity to release, review, hold", () => {
+ assert.equal(decisionFromFindings([]), "RELEASE");
+ assert.equal(decisionFromFindings([{ severity: "minor" }]), "REVIEW");
+ assert.equal(decisionFromFindings([{ severity: "critical" }]), "HOLD");
+});