diff --git a/collaborative-chat-mention-scope-guard/README.md b/collaborative-chat-mention-scope-guard/README.md
new file mode 100644
index 00000000..47c19b80
--- /dev/null
+++ b/collaborative-chat-mention-scope-guard/README.md
@@ -0,0 +1,34 @@
+# Collaborative Chat Mention Scope Guard
+
+This module is a focused real-time collaborative editor slice for issue #12. It
+checks whether document chat and sidebar `@mentions` are safe before alerts or
+in-app notifications are created.
+
+The guard uses synthetic data only and has no external service dependencies.
+
+## Checks
+
+- Mentioned collaborators who are not members of the target section.
+- Blinded reviewer identities mentioned in author-visible threads.
+- External collaborators mentioned in restricted sections.
+- Notebook-cell mentions that target users without cell access.
+- Mentions inside locked sections that require owner approval.
+- Redaction actions for private section titles before fanout.
+
+## Local Verification
+
+```bash
+node collaborative-chat-mention-scope-guard/test.js
+node collaborative-chat-mention-scope-guard/demo.js
+```
+
+Demo artifacts are written to
+`collaborative-chat-mention-scope-guard/reports/`.
+
+## Issue #12 Mapping
+
+This maps to the editor's document chat, section collaboration, notebook-cell
+annotation, user presence, and controlled-section workflows. It is separate from
+discussion sidebar auditing, notification visibility, private-comment export,
+presence privacy/liveness, suggestion provenance, clipboard import, local cache,
+find/replace, data availability, and section-lock arbitration slices.
diff --git a/collaborative-chat-mention-scope-guard/demo.js b/collaborative-chat-mention-scope-guard/demo.js
new file mode 100644
index 00000000..023644fe
--- /dev/null
+++ b/collaborative-chat-mention-scope-guard/demo.js
@@ -0,0 +1,16 @@
+const fs = require("fs");
+const path = require("path");
+const { evaluateMentionFanout, toMarkdown, toSvg } = require("./mentionScopeGuard");
+const sampleWorkspace = require("./sampleWorkspace");
+
+const result = evaluateMentionFanout(sampleWorkspace);
+const reportDir = path.join(__dirname, "reports");
+
+fs.mkdirSync(reportDir, { recursive: true });
+fs.writeFileSync(path.join(reportDir, "mention-scope-packet.json"), `${JSON.stringify(result, null, 2)}\n`);
+fs.writeFileSync(path.join(reportDir, "mention-scope-report.md"), toMarkdown(result));
+fs.writeFileSync(path.join(reportDir, "summary.svg"), toSvg(result));
+
+console.log(`decision=${result.decision}`);
+console.log(`findings=${result.findings.length}`);
+console.log(`reports=${reportDir}`);
diff --git a/collaborative-chat-mention-scope-guard/mentionScopeGuard.js b/collaborative-chat-mention-scope-guard/mentionScopeGuard.js
new file mode 100644
index 00000000..bf0cf6ad
--- /dev/null
+++ b/collaborative-chat-mention-scope-guard/mentionScopeGuard.js
@@ -0,0 +1,160 @@
+function addFinding(findings, severity, id, message, action, evidence = {}) {
+ findings.push({ severity, id, message, action, evidence });
+}
+
+function evaluateMentionFanout(workspace) {
+ const findings = [];
+ const usersById = new Map((workspace.users || []).map((user) => [user.id, user]));
+ const sectionsById = new Map((workspace.sections || []).map((section) => [section.id, section]));
+ const cellsById = new Map((workspace.notebookCells || []).map((cell) => [cell.id, cell]));
+
+ for (const thread of workspace.chatThreads || []) {
+ const section = sectionsById.get(thread.sectionId);
+
+ if (!section) {
+ addFinding(
+ findings,
+ "block",
+ `unknown-section:${thread.id}`,
+ `Thread ${thread.id} targets an unknown document section.`,
+ "Resolve the thread section before creating mention fanout.",
+ { threadId: thread.id, sectionId: thread.sectionId }
+ );
+ continue;
+ }
+
+ for (const mentionId of thread.mentions || []) {
+ const user = usersById.get(mentionId);
+ if (!user) {
+ addFinding(
+ findings,
+ "block",
+ `unknown-mention:${thread.id}:${mentionId}`,
+ `Thread ${thread.id} mentions an unknown collaborator ${mentionId}.`,
+ "Drop or resolve unknown collaborators before fanout.",
+ { threadId: thread.id, mentionId }
+ );
+ continue;
+ }
+
+ if (!section.members.includes(mentionId)) {
+ addFinding(
+ findings,
+ "hold",
+ `section-scope:${thread.id}:${mentionId}`,
+ `${user.displayName} is mentioned outside their allowed section scope.`,
+ "Invite the collaborator to the section or remove the mention.",
+ { threadId: thread.id, sectionId: section.id, mentionId }
+ );
+ }
+
+ if (user.role === "blinded-reviewer" && thread.visibility === "author-visible") {
+ addFinding(
+ findings,
+ "block",
+ `blinded-reviewer-leak:${thread.id}:${mentionId}`,
+ `Author-visible thread ${thread.id} would reveal blinded reviewer ${user.displayName}.`,
+ "Redact the reviewer mention or move the message to a reviewer-only thread.",
+ { threadId: thread.id, mentionId }
+ );
+ }
+
+ if (user.external && section.restricted) {
+ addFinding(
+ findings,
+ "block",
+ `external-restricted-section:${thread.id}:${mentionId}`,
+ `External collaborator ${user.displayName} is mentioned in restricted section ${section.id}.`,
+ "Remove the mention or approve restricted-section access before fanout.",
+ { threadId: thread.id, sectionId: section.id, mentionId }
+ );
+ }
+
+ if (thread.notebookCellId) {
+ const cell = cellsById.get(thread.notebookCellId);
+ if (!cell || !cell.allowedUsers.includes(mentionId)) {
+ addFinding(
+ findings,
+ "hold",
+ `notebook-cell-scope:${thread.id}:${mentionId}`,
+ `${user.displayName} is mentioned on notebook cell ${thread.notebookCellId} without cell access.`,
+ "Grant cell access or move the mention to a document-level thread.",
+ { threadId: thread.id, notebookCellId: thread.notebookCellId, mentionId }
+ );
+ }
+ }
+ }
+
+ if (section.locked && thread.mentions.length > 0 && !thread.sectionOwnerApproved) {
+ addFinding(
+ findings,
+ "hold",
+ `locked-section-mention:${thread.id}`,
+ `Thread ${thread.id} mentions collaborators inside locked section ${section.id} without owner approval.`,
+ "Ask the section owner to approve the mention fanout or defer it until unlock.",
+ { threadId: thread.id, sectionId: section.id }
+ );
+ }
+
+ if (section.privateTitle && thread.fanoutPreview && thread.fanoutPreview.includes(section.title)) {
+ addFinding(
+ findings,
+ "hold",
+ `private-title-preview:${thread.id}`,
+ `Thread ${thread.id} fanout preview includes a private section title.`,
+ "Redact the section title in the mention preview before alerting collaborators.",
+ { threadId: thread.id, sectionId: section.id }
+ );
+ }
+ }
+
+ const blockers = findings.filter((finding) => finding.severity === "block").length;
+ const holds = findings.filter((finding) => finding.severity === "hold").length;
+
+ return {
+ workspaceId: workspace.id,
+ decision: blockers > 0 ? "block-mention-fanout" : holds > 0 ? "hold-for-thread-owner" : "ready-for-mention-fanout",
+ summary: {
+ threads: (workspace.chatThreads || []).length,
+ blockers,
+ holds,
+ findings: findings.length
+ },
+ findings
+ };
+}
+
+function toMarkdown(result) {
+ const rows = result.findings.map((finding) => `| ${finding.severity} | ${finding.message} | ${finding.action} |`);
+ return [
+ "# Chat Mention Scope Guard Report",
+ "",
+ `Workspace: ${result.workspaceId}`,
+ `Decision: ${result.decision}`,
+ "",
+ "| Severity | Finding | Action |",
+ "| --- | --- | --- |",
+ ...(rows.length ? rows : ["| ok | No mention-scope issues found. | Mention fanout may continue. |"]),
+ ""
+ ].join("\n");
+}
+
+function toSvg(result) {
+ const color = result.decision === "ready-for-mention-fanout" ? "#0f7b45" : result.decision === "hold-for-thread-owner" ? "#9a6700" : "#b42318";
+ return [
+ ``
+ ].join("\n");
+}
+
+module.exports = {
+ evaluateMentionFanout,
+ toMarkdown,
+ toSvg
+};
diff --git a/collaborative-chat-mention-scope-guard/reports/demo.webm b/collaborative-chat-mention-scope-guard/reports/demo.webm
new file mode 100644
index 00000000..05a85d93
Binary files /dev/null and b/collaborative-chat-mention-scope-guard/reports/demo.webm differ
diff --git a/collaborative-chat-mention-scope-guard/reports/mention-scope-packet.json b/collaborative-chat-mention-scope-guard/reports/mention-scope-packet.json
new file mode 100644
index 00000000..9c04a957
--- /dev/null
+++ b/collaborative-chat-mention-scope-guard/reports/mention-scope-packet.json
@@ -0,0 +1,97 @@
+{
+ "workspaceId": "editor-room-chat-118",
+ "decision": "block-mention-fanout",
+ "summary": {
+ "threads": 2,
+ "blockers": 2,
+ "holds": 6,
+ "findings": 8
+ },
+ "findings": [
+ {
+ "severity": "hold",
+ "id": "section-scope:thread-1:u-reviewer",
+ "message": "Reviewer B is mentioned outside their allowed section scope.",
+ "action": "Invite the collaborator to the section or remove the mention.",
+ "evidence": {
+ "threadId": "thread-1",
+ "sectionId": "methods",
+ "mentionId": "u-reviewer"
+ }
+ },
+ {
+ "severity": "block",
+ "id": "blinded-reviewer-leak:thread-1:u-reviewer",
+ "message": "Author-visible thread thread-1 would reveal blinded reviewer Reviewer B.",
+ "action": "Redact the reviewer mention or move the message to a reviewer-only thread.",
+ "evidence": {
+ "threadId": "thread-1",
+ "mentionId": "u-reviewer"
+ }
+ },
+ {
+ "severity": "hold",
+ "id": "notebook-cell-scope:thread-1:u-reviewer",
+ "message": "Reviewer B is mentioned on notebook cell cell-qc-4 without cell access.",
+ "action": "Grant cell access or move the mention to a document-level thread.",
+ "evidence": {
+ "threadId": "thread-1",
+ "notebookCellId": "cell-qc-4",
+ "mentionId": "u-reviewer"
+ }
+ },
+ {
+ "severity": "hold",
+ "id": "section-scope:thread-1:u-contractor",
+ "message": "External Analyst is mentioned outside their allowed section scope.",
+ "action": "Invite the collaborator to the section or remove the mention.",
+ "evidence": {
+ "threadId": "thread-1",
+ "sectionId": "methods",
+ "mentionId": "u-contractor"
+ }
+ },
+ {
+ "severity": "block",
+ "id": "external-restricted-section:thread-1:u-contractor",
+ "message": "External collaborator External Analyst is mentioned in restricted section methods.",
+ "action": "Remove the mention or approve restricted-section access before fanout.",
+ "evidence": {
+ "threadId": "thread-1",
+ "sectionId": "methods",
+ "mentionId": "u-contractor"
+ }
+ },
+ {
+ "severity": "hold",
+ "id": "notebook-cell-scope:thread-1:u-contractor",
+ "message": "External Analyst is mentioned on notebook cell cell-qc-4 without cell access.",
+ "action": "Grant cell access or move the mention to a document-level thread.",
+ "evidence": {
+ "threadId": "thread-1",
+ "notebookCellId": "cell-qc-4",
+ "mentionId": "u-contractor"
+ }
+ },
+ {
+ "severity": "hold",
+ "id": "locked-section-mention:thread-1",
+ "message": "Thread thread-1 mentions collaborators inside locked section methods without owner approval.",
+ "action": "Ask the section owner to approve the mention fanout or defer it until unlock.",
+ "evidence": {
+ "threadId": "thread-1",
+ "sectionId": "methods"
+ }
+ },
+ {
+ "severity": "hold",
+ "id": "private-title-preview:thread-1",
+ "message": "Thread thread-1 fanout preview includes a private section title.",
+ "action": "Redact the section title in the mention preview before alerting collaborators.",
+ "evidence": {
+ "threadId": "thread-1",
+ "sectionId": "methods"
+ }
+ }
+ ]
+}
diff --git a/collaborative-chat-mention-scope-guard/reports/mention-scope-report.md b/collaborative-chat-mention-scope-guard/reports/mention-scope-report.md
new file mode 100644
index 00000000..bf3cc1cb
--- /dev/null
+++ b/collaborative-chat-mention-scope-guard/reports/mention-scope-report.md
@@ -0,0 +1,15 @@
+# Chat Mention Scope Guard Report
+
+Workspace: editor-room-chat-118
+Decision: block-mention-fanout
+
+| Severity | Finding | Action |
+| --- | --- | --- |
+| hold | Reviewer B is mentioned outside their allowed section scope. | Invite the collaborator to the section or remove the mention. |
+| block | Author-visible thread thread-1 would reveal blinded reviewer Reviewer B. | Redact the reviewer mention or move the message to a reviewer-only thread. |
+| hold | Reviewer B is mentioned on notebook cell cell-qc-4 without cell access. | Grant cell access or move the mention to a document-level thread. |
+| hold | External Analyst is mentioned outside their allowed section scope. | Invite the collaborator to the section or remove the mention. |
+| block | External collaborator External Analyst is mentioned in restricted section methods. | Remove the mention or approve restricted-section access before fanout. |
+| hold | External Analyst is mentioned on notebook cell cell-qc-4 without cell access. | Grant cell access or move the mention to a document-level thread. |
+| hold | Thread thread-1 mentions collaborators inside locked section methods without owner approval. | Ask the section owner to approve the mention fanout or defer it until unlock. |
+| hold | Thread thread-1 fanout preview includes a private section title. | Redact the section title in the mention preview before alerting collaborators. |
diff --git a/collaborative-chat-mention-scope-guard/reports/summary.svg b/collaborative-chat-mention-scope-guard/reports/summary.svg
new file mode 100644
index 00000000..d04c6d0d
--- /dev/null
+++ b/collaborative-chat-mention-scope-guard/reports/summary.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/collaborative-chat-mention-scope-guard/sampleWorkspace.js b/collaborative-chat-mention-scope-guard/sampleWorkspace.js
new file mode 100644
index 00000000..bfbabd94
--- /dev/null
+++ b/collaborative-chat-mention-scope-guard/sampleWorkspace.js
@@ -0,0 +1,49 @@
+module.exports = {
+ id: "editor-room-chat-118",
+ users: [
+ { id: "u-author", displayName: "Dr. Author", role: "author", external: false },
+ { id: "u-stat", displayName: "Dr. Statistician", role: "collaborator", external: false },
+ { id: "u-reviewer", displayName: "Reviewer B", role: "blinded-reviewer", external: false },
+ { id: "u-contractor", displayName: "External Analyst", role: "collaborator", external: true }
+ ],
+ sections: [
+ {
+ id: "methods",
+ title: "Restricted Assay Methods",
+ members: ["u-author", "u-stat"],
+ restricted: true,
+ locked: true,
+ privateTitle: true
+ },
+ {
+ id: "discussion",
+ title: "Discussion",
+ members: ["u-author", "u-stat", "u-contractor"],
+ restricted: false,
+ locked: false,
+ privateTitle: false
+ }
+ ],
+ notebookCells: [
+ { id: "cell-qc-4", allowedUsers: ["u-author", "u-stat"] }
+ ],
+ chatThreads: [
+ {
+ id: "thread-1",
+ sectionId: "methods",
+ visibility: "author-visible",
+ mentions: ["u-reviewer", "u-contractor", "u-stat"],
+ notebookCellId: "cell-qc-4",
+ sectionOwnerApproved: false,
+ fanoutPreview: "New mention in Restricted Assay Methods"
+ },
+ {
+ id: "thread-2",
+ sectionId: "discussion",
+ visibility: "project-visible",
+ mentions: ["u-contractor"],
+ sectionOwnerApproved: true,
+ fanoutPreview: "New mention in Discussion"
+ }
+ ]
+};
diff --git a/collaborative-chat-mention-scope-guard/test.js b/collaborative-chat-mention-scope-guard/test.js
new file mode 100644
index 00000000..0a1a58ef
--- /dev/null
+++ b/collaborative-chat-mention-scope-guard/test.js
@@ -0,0 +1,27 @@
+const assert = require("assert");
+const { evaluateMentionFanout } = require("./mentionScopeGuard");
+const sampleWorkspace = require("./sampleWorkspace");
+
+const result = evaluateMentionFanout(sampleWorkspace);
+
+assert.equal(result.decision, "block-mention-fanout");
+assert.ok(result.findings.some((finding) => finding.id === "section-scope:thread-1:u-reviewer"));
+assert.ok(result.findings.some((finding) => finding.id === "blinded-reviewer-leak:thread-1:u-reviewer"));
+assert.ok(result.findings.some((finding) => finding.id === "section-scope:thread-1:u-contractor"));
+assert.ok(result.findings.some((finding) => finding.id === "external-restricted-section:thread-1:u-contractor"));
+assert.ok(result.findings.some((finding) => finding.id === "notebook-cell-scope:thread-1:u-contractor"));
+assert.ok(result.findings.some((finding) => finding.id === "locked-section-mention:thread-1"));
+assert.ok(result.findings.some((finding) => finding.id === "private-title-preview:thread-1"));
+
+const cleanResult = evaluateMentionFanout({
+ id: "clean-editor-room",
+ users: [{ id: "u-a", displayName: "Author", role: "author", external: false }],
+ sections: [{ id: "intro", title: "Intro", members: ["u-a"], restricted: false, locked: false, privateTitle: false }],
+ notebookCells: [],
+ chatThreads: [{ id: "t-ok", sectionId: "intro", visibility: "project-visible", mentions: ["u-a"], sectionOwnerApproved: true, fanoutPreview: "New mention in Intro" }]
+});
+
+assert.equal(cleanResult.decision, "ready-for-mention-fanout");
+assert.equal(cleanResult.findings.length, 0);
+
+console.log("chat mention scope guard tests passed");