diff --git a/collaborative-glossary-acronym-guard/.gitignore b/collaborative-glossary-acronym-guard/.gitignore
new file mode 100644
index 00000000..2bf074d6
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/.gitignore
@@ -0,0 +1 @@
+reports/frames/
diff --git a/collaborative-glossary-acronym-guard/README.md b/collaborative-glossary-acronym-guard/README.md
new file mode 100644
index 00000000..650ecbfb
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/README.md
@@ -0,0 +1,42 @@
+# Collaborative Glossary and Acronym Guard
+
+Self-contained reviewer artifact for SCIBASE issue #12, focused on terminology consistency inside a real-time collaborative research editor.
+
+This slice is intentionally narrow. It validates glossary and acronym readiness before manuscript export rather than duplicating existing editor, notebook, citation, equation, table, clipboard, accessibility, data-availability, or mode-toggle submissions.
+
+## What It Checks
+
+- First-use acronym expansion before publication export.
+- Acronym collisions when collaborators use the same abbreviation for different concepts.
+- Missing, draft, or incomplete glossary definitions.
+- Export manifest completeness for terms that appear in manuscript sections.
+- Locked-section terminology edits that lack current collaborator approval.
+- Stale term anchors after section version changes.
+- Private reviewer, anonymous-review, local-path, or collaborator-only terminology notes leaking into public exports.
+- Blocking terminology review comments.
+
+## Local Verification
+
+```sh
+npm run check
+npm test
+npm run demo
+npm run verify-video
+```
+
+`npm run demo` writes reviewer artifacts to `reports/`:
+
+- `clean-audit.json`
+- `risky-audit.json`
+- `risky-review.md`
+- `summary.svg`
+- `manifest.json`
+- `demo.mp4`
+
+## Requirement Mapping
+
+- Real-time collaboration: proposed terminology edits are evaluated against section locks, current section hashes, and collaborator approvals.
+- Research editor interface: glossary entries bind to manuscript sections and export manifests.
+- Scientific writing support: acronyms, first-use expansion, discipline-specific definitions, and term collisions are checked deterministically.
+- Export readiness: public glossary output is separated from private notes and unresolved terminology comments.
+- Reviewer demonstration: synthetic clean and risky packets produce JSON, Markdown, SVG, and MP4 artifacts without credentials or external services.
diff --git a/collaborative-glossary-acronym-guard/demo.js b/collaborative-glossary-acronym-guard/demo.js
new file mode 100644
index 00000000..910ec8c8
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/demo.js
@@ -0,0 +1,52 @@
+"use strict";
+
+const fs = require("node:fs");
+const path = require("node:path");
+const {
+ evaluateGlossaryAcronymPacket,
+ renderMarkdownReport,
+ renderSvgSummary
+} = require("./index");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const clean = evaluateGlossaryAcronymPacket(cleanPacket, { now: "2026-06-01T10:00:00.000Z" });
+const risky = evaluateGlossaryAcronymPacket(riskyPacket, { now: "2026-06-01T10:00:00.000Z" });
+const manifest = {
+ module: "collaborative-glossary-acronym-guard",
+ issue: 12,
+ generatedAt: "2026-06-01T10:00:00.000Z",
+ scenarios: [
+ {
+ name: "clean",
+ status: clean.status,
+ fingerprint: clean.fingerprint,
+ findings: clean.findings.length
+ },
+ {
+ name: "risky",
+ status: risky.status,
+ fingerprint: risky.fingerprint,
+ findings: risky.findings.length
+ }
+ ],
+ artifacts: [
+ "reports/clean-audit.json",
+ "reports/risky-audit.json",
+ "reports/risky-review.md",
+ "reports/summary.svg",
+ "reports/demo.mp4"
+ ]
+};
+
+fs.writeFileSync(path.join(reportsDir, "clean-audit.json"), `${JSON.stringify(clean, null, 2)}\n`);
+fs.writeFileSync(path.join(reportsDir, "risky-audit.json"), `${JSON.stringify(risky, null, 2)}\n`);
+fs.writeFileSync(path.join(reportsDir, "risky-review.md"), renderMarkdownReport(risky, riskyPacket));
+fs.writeFileSync(path.join(reportsDir, "summary.svg"), renderSvgSummary(risky));
+fs.writeFileSync(path.join(reportsDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
+
+console.log(`Clean status: ${clean.status} (${clean.fingerprint})`);
+console.log(`Risky status: ${risky.status} (${risky.fingerprint})`);
+console.log(`Wrote reviewer artifacts to ${reportsDir}`);
diff --git a/collaborative-glossary-acronym-guard/index.js b/collaborative-glossary-acronym-guard/index.js
new file mode 100644
index 00000000..19321f15
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/index.js
@@ -0,0 +1,515 @@
+"use strict";
+
+const crypto = require("node:crypto");
+
+const SEVERITY_ORDER = ["critical", "high", "warning", "info"];
+const PRIVATE_NOTE_PATTERN =
+ /\b(?:reviewer-only|blind-review|anonymous reviewer|internal note|\/Users\/|C:\\Users\\|patient-id|phi:|private collaborator|do not export)\b/i;
+
+function evaluateGlossaryAcronymPacket(packet, options = {}) {
+ if (!isPlainObject(packet)) {
+ throw new TypeError("evaluateGlossaryAcronymPacket expects a packet object");
+ }
+
+ const now = options.now ?? new Date().toISOString();
+ const minDefinitionWords = options.minDefinitionWords ?? 6;
+ const findings = [];
+ const document = isPlainObject(packet.document) ? packet.document : {};
+ const sections = asArray(packet.sections);
+ const glossaryEntries = asArray(packet.glossaryEntries);
+ const proposedEdits = asArray(packet.proposedEdits);
+ const comments = asArray(packet.comments);
+ const exportSettings = isPlainObject(packet.export) ? packet.export : {};
+ const sectionById = new Map(sections.map((section) => [String(section.id), section]));
+ const entryById = new Map(glossaryEntries.map((entry) => [String(entry.id), entry]));
+ const includeGlossaryIds = new Set(asArray(exportSettings.includeGlossaryIds).map(String));
+
+ if (!document.id || !document.title || !document.versionHash) {
+ findings.push(
+ finding(
+ "PACKET_SCHEMA_MISSING_DOCUMENT",
+ "high",
+ "Glossary packet is missing a stable document id, title, or version hash.",
+ "Collaborative export review needs manuscript metadata to bind terminology decisions.",
+ "document",
+ "Attach stable document metadata before the terminology guard runs."
+ )
+ );
+ }
+
+ if (sections.length === 0) {
+ findings.push(
+ finding(
+ "PACKET_SCHEMA_MISSING_SECTIONS",
+ "high",
+ "Glossary packet has no manuscript sections to inspect.",
+ "The guard cannot verify first-use acronym expansion or locked-section terminology edits.",
+ "sections",
+ "Attach the exportable manuscript sections."
+ )
+ );
+ }
+
+ if (glossaryEntries.length === 0) {
+ findings.push(
+ finding(
+ "PACKET_SCHEMA_MISSING_GLOSSARY",
+ "high",
+ "Glossary packet has no glossary entries.",
+ "Collaborative terminology export cannot prove that scientific terms are defined.",
+ "glossaryEntries",
+ "Attach approved glossary entries before export."
+ )
+ );
+ }
+
+ inspectAcronymCollisions(glossaryEntries, findings);
+ inspectAcronymFirstUse(sections, glossaryEntries, findings);
+ inspectGlossaryEntries(glossaryEntries, sectionById, includeGlossaryIds, exportSettings, minDefinitionWords, findings);
+ inspectMentionedTerms(sections, glossaryEntries, includeGlossaryIds, findings);
+ inspectLockedEdits(proposedEdits, sectionById, entryById, findings);
+ inspectBlockingComments(comments, entryById, findings);
+
+ const sortedFindings = sortFindings(findings);
+ const status = determineStatus(sortedFindings);
+ const summary = summarize(status, sortedFindings);
+ const remediationActions = sortedFindings.map((item) => ({
+ code: item.code,
+ owner: item.owner,
+ action: item.remediation
+ }));
+ const exportGlossary = glossaryEntries
+ .filter((entry) => includeGlossaryIds.has(String(entry.id)))
+ .map((entry) => ({
+ id: entry.id,
+ term: entry.term,
+ acronym: entry.acronym ?? null,
+ expansion: entry.expansion ?? null,
+ definition: entry.definition,
+ discipline: entry.discipline ?? "general"
+ }));
+ const fingerprint = crypto
+ .createHash("sha256")
+ .update(
+ JSON.stringify({
+ document,
+ sections: sections.map((section) => ({
+ id: section.id,
+ versionHash: section.versionHash,
+ body: section.body
+ })),
+ glossaryEntries,
+ proposedEdits,
+ exportSettings,
+ codes: sortedFindings.map((item) => item.code)
+ })
+ )
+ .digest("hex")
+ .slice(0, 16);
+
+ return {
+ generatedAt: now,
+ status,
+ summary,
+ findingCounts: countBySeverity(sortedFindings),
+ findings: sortedFindings,
+ exportGlossary,
+ remediationActions,
+ fingerprint
+ };
+}
+
+function renderMarkdownReport(result, packet) {
+ const lines = [
+ `# Collaborative Glossary and Acronym Guard`,
+ "",
+ `Document: ${packet.document?.title ?? "Untitled"}`,
+ `Status: ${result.status}`,
+ `Fingerprint: ${result.fingerprint}`,
+ "",
+ "## Summary",
+ "",
+ result.summary,
+ "",
+ "## Findings",
+ ""
+ ];
+
+ if (result.findings.length === 0) {
+ lines.push("- No terminology blockers found.");
+ } else {
+ result.findings.forEach((item) => {
+ lines.push(`- ${item.severity.toUpperCase()} ${item.code}: ${item.message}`);
+ lines.push(` - Remediation: ${item.remediation}`);
+ });
+ }
+
+ lines.push("", "## Export Glossary", "");
+ result.exportGlossary.forEach((entry) => {
+ const label = entry.acronym ? `${entry.acronym} - ${entry.expansion}` : entry.term;
+ lines.push(`- ${label}: ${entry.definition}`);
+ });
+
+ return `${lines.join("\n")}\n`;
+}
+
+function renderSvgSummary(result) {
+ const counts = result.findingCounts;
+ const critical = counts.critical ?? 0;
+ const high = counts.high ?? 0;
+ const warning = counts.warning ?? 0;
+ const ready = result.status === "READY";
+ const statusColor = ready ? "#16794c" : result.status === "REVISE" ? "#a15c00" : "#a11b32";
+ const holdWidth = Math.min(300, (critical + high) * 60);
+ const warningWidth = Math.min(220, warning * 55);
+ const readyWidth = ready ? 260 : Math.max(60, 260 - holdWidth);
+
+ return [
+ ``
+ ].join("\n");
+}
+
+function inspectAcronymCollisions(glossaryEntries, findings) {
+ const byAcronym = new Map();
+ glossaryEntries.forEach((entry) => {
+ if (!entry.acronym || !entry.expansion) {
+ return;
+ }
+ const acronym = normalizeAcronym(entry.acronym);
+ const expansion = normalizeText(entry.expansion);
+ if (!byAcronym.has(acronym)) {
+ byAcronym.set(acronym, new Map());
+ }
+ if (!byAcronym.get(acronym).has(expansion)) {
+ byAcronym.get(acronym).set(expansion, []);
+ }
+ byAcronym.get(acronym).get(expansion).push(entry);
+ });
+
+ for (const [acronym, expansions] of byAcronym) {
+ if (expansions.size <= 1) {
+ continue;
+ }
+ const labels = [...expansions.keys()].join("; ");
+ findings.push(
+ finding(
+ "ACRONYM_EXPANSION_COLLISION",
+ "critical",
+ `${acronym} has multiple approved expansions: ${labels}.`,
+ "Readers and downstream exports cannot know which definition applies in each section.",
+ `glossaryEntries.${acronym}`,
+ "Split the acronym by discipline or choose one canonical manuscript-level expansion.",
+ "lead editor"
+ )
+ );
+ }
+}
+
+function inspectAcronymFirstUse(sections, glossaryEntries, findings) {
+ glossaryEntries
+ .filter((entry) => entry.acronym && entry.expansion)
+ .forEach((entry) => {
+ const acronym = normalizeAcronym(entry.acronym);
+ const expansion = String(entry.expansion);
+ const firstUse = findFirstUse(sections, acronym);
+ if (!firstUse) {
+ return;
+ }
+ const section = firstUse.section;
+ const before = String(section.body ?? "").slice(Math.max(0, firstUse.index - expansion.length - 12), firstUse.index + acronym.length + 2);
+ const expected = new RegExp(`${escapeRegex(expansion)}\\s*\\(\\s*${escapeRegex(acronym)}\\s*\\)`, "i");
+ if (!expected.test(before)) {
+ findings.push(
+ finding(
+ "ACRONYM_FIRST_USE_UNEXPANDED",
+ "high",
+ `${acronym} first appears in section ${section.id} without its expansion.`,
+ "Publication exports should define acronyms at first use for readers and reviewers.",
+ `sections.${section.id}`,
+ `Introduce "${expansion} (${acronym})" at first use or move the first mention after the definition.`,
+ "section author"
+ )
+ );
+ }
+ });
+}
+
+function inspectGlossaryEntries(glossaryEntries, sectionById, includeGlossaryIds, exportSettings, minDefinitionWords, findings) {
+ glossaryEntries.forEach((entry, index) => {
+ const path = `glossaryEntries[${index}]`;
+ const id = String(entry.id ?? "");
+ const term = String(entry.term ?? entry.acronym ?? `entry ${index}`);
+
+ if (!id) {
+ findings.push(
+ finding(
+ "GLOSSARY_ENTRY_MISSING_ID",
+ "high",
+ `Glossary entry "${term}" has no stable id.`,
+ "Collaborative approvals and export manifests need stable terminology identifiers.",
+ `${path}.id`,
+ "Assign a stable glossary id.",
+ "terminology editor"
+ )
+ );
+ }
+
+ if (entry.exportable !== false && !includeGlossaryIds.has(id)) {
+ findings.push(
+ finding(
+ "GLOSSARY_EXPORT_INCOMPLETE",
+ "high",
+ `Exportable glossary entry "${term}" is missing from the export glossary manifest.`,
+ "Readers will see terminology in the manuscript without a matching glossary definition.",
+ `${path}.exportable`,
+ "Add the entry id to export.includeGlossaryIds or mark it as internal-only.",
+ "export owner"
+ )
+ );
+ }
+
+ if (!entry.definition || countWords(entry.definition) < minDefinitionWords) {
+ findings.push(
+ finding(
+ "TERM_DEFINITION_INCOMPLETE",
+ "high",
+ `Glossary entry "${term}" has an incomplete definition.`,
+ "Short or missing definitions create discipline-specific ambiguity during review.",
+ `${path}.definition`,
+ "Add a concise, reviewer-ready definition with enough scientific context.",
+ "terminology editor"
+ )
+ );
+ }
+
+ if (entry.status !== "approved" && entry.exportable !== false) {
+ findings.push(
+ finding(
+ "UNAPPROVED_GLOSSARY_TERM",
+ "high",
+ `Glossary entry "${term}" is exportable but not approved.`,
+ "Collaborative manuscripts should not export provisional terminology.",
+ `${path}.status`,
+ "Route the term through collaborator approval or hold it from export.",
+ "lead editor"
+ )
+ );
+ }
+
+ const section = sectionById.get(String(entry.firstUseSectionId ?? ""));
+ if (entry.firstUseSectionId && !section) {
+ findings.push(
+ finding(
+ "TERM_FIRST_USE_SECTION_UNKNOWN",
+ "high",
+ `Glossary entry "${term}" references an unknown first-use section.`,
+ "Glossary anchors must point to live manuscript sections.",
+ `${path}.firstUseSectionId`,
+ "Bind the term to a current section id.",
+ "terminology editor"
+ )
+ );
+ }
+
+ if (section && entry.sectionVersionHash && entry.sectionVersionHash !== section.versionHash) {
+ findings.push(
+ finding(
+ "STALE_TERM_SECTION_ANCHOR",
+ "warning",
+ `Glossary entry "${term}" was approved against an older version of section ${section.id}.`,
+ "Collaborative edits may have changed the term context since approval.",
+ `${path}.sectionVersionHash`,
+ "Refresh the term anchor against the current section version.",
+ "section author"
+ )
+ );
+ }
+
+ const exportNote = String(entry.exportNote ?? "");
+ const privateNote = String(entry.privateNote ?? "");
+ if (PRIVATE_NOTE_PATTERN.test(exportNote) || (exportSettings.includePrivateNotes === true && PRIVATE_NOTE_PATTERN.test(privateNote))) {
+ findings.push(
+ finding(
+ "PRIVATE_TERMINOLOGY_NOTE_LEAK",
+ "critical",
+ `Glossary entry "${term}" would leak private reviewer or collaborator terminology notes.`,
+ "Anonymous review notes, local paths, and private comments must not appear in manuscript exports.",
+ `${path}.exportNote`,
+ "Redact private terminology notes and export only public glossary text.",
+ "export owner"
+ )
+ );
+ }
+ });
+}
+
+function inspectMentionedTerms(sections, glossaryEntries, includeGlossaryIds, findings) {
+ const exportableEntries = glossaryEntries.filter((entry) => entry.exportable !== false && (entry.term || entry.acronym));
+ exportableEntries.forEach((entry) => {
+ const token = String(entry.term ?? entry.acronym);
+ if (includeGlossaryIds.has(String(entry.id))) {
+ return;
+ }
+ const matcher = new RegExp(`\\b${escapeRegex(token)}\\b`, "i");
+ const section = sections.find((candidate) => matcher.test(String(candidate.body ?? "")));
+ if (!section) {
+ return;
+ }
+ findings.push(
+ finding(
+ "MENTIONED_TERM_NOT_EXPORTED",
+ "high",
+ `"${token}" appears in section ${section.id} but is absent from the exported glossary.`,
+ "Collaborators can accidentally remove a needed term from the reader-facing glossary.",
+ `sections.${section.id}`,
+ "Include the mentioned term in the export glossary or remove the stale manuscript mention.",
+ "export owner"
+ )
+ );
+ });
+}
+
+function inspectLockedEdits(proposedEdits, sectionById, entryById, findings) {
+ proposedEdits.forEach((edit, index) => {
+ const section = sectionById.get(String(edit.sectionId ?? ""));
+ const entry = entryById.get(String(edit.termId ?? ""));
+ if (!section || section.locked !== true) {
+ return;
+ }
+ const approvals = asArray(edit.approvedBy);
+ if (approvals.length === 0 || edit.sectionVersionHash !== section.versionHash) {
+ findings.push(
+ finding(
+ "LOCKED_SECTION_TERM_EDIT_UNAPPROVED",
+ "high",
+ `Terminology edit ${edit.id ?? index} targets locked section ${section.id} without current approval.`,
+ "Locked final-review sections should not accept glossary changes without explicit approval.",
+ `proposedEdits[${index}]`,
+ `Obtain approval for ${entry?.term ?? edit.termId ?? "the term"} against section version ${section.versionHash}.`,
+ "section owner"
+ )
+ );
+ }
+ });
+}
+
+function inspectBlockingComments(comments, entryById, findings) {
+ comments.forEach((comment, index) => {
+ if (comment.resolved === true || comment.blocksExport !== true) {
+ return;
+ }
+ const entry = entryById.get(String(comment.termId ?? ""));
+ findings.push(
+ finding(
+ "BLOCKING_TERMINOLOGY_COMMENT",
+ "high",
+ `Unresolved terminology comment blocks export for ${entry?.term ?? comment.termId ?? "a glossary term"}.`,
+ "The collaborative editor should not publish terminology while blocking review threads are open.",
+ `comments[${index}]`,
+ "Resolve the terminology comment or mark it non-blocking before export.",
+ "review owner"
+ )
+ );
+ });
+}
+
+function findFirstUse(sections, acronym) {
+ const matcher = new RegExp(`\\b${escapeRegex(acronym)}\\b`);
+ for (const section of sections) {
+ const body = String(section.body ?? "");
+ const match = matcher.exec(body);
+ if (match) {
+ return { section, index: match.index };
+ }
+ }
+ return null;
+}
+
+function determineStatus(findings) {
+ if (findings.some((item) => item.severity === "critical" || item.severity === "high")) {
+ return "HOLD";
+ }
+ if (findings.some((item) => item.severity === "warning")) {
+ return "REVISE";
+ }
+ return "READY";
+}
+
+function summarize(status, findings) {
+ if (status === "READY") {
+ return "Glossary, acronym, and terminology export checks passed for the collaborative manuscript.";
+ }
+ const counts = countBySeverity(findings);
+ return `Glossary export is ${status.toLowerCase()} with ${counts.critical ?? 0} critical, ${counts.high ?? 0} high, and ${counts.warning ?? 0} warning finding(s).`;
+}
+
+function finding(code, severity, message, impact, path, remediation, owner = "editor") {
+ return { code, severity, message, impact, path, remediation, owner };
+}
+
+function sortFindings(findings) {
+ return findings.sort((left, right) => {
+ const severityDelta = SEVERITY_ORDER.indexOf(left.severity) - SEVERITY_ORDER.indexOf(right.severity);
+ if (severityDelta !== 0) {
+ return severityDelta;
+ }
+ return left.code.localeCompare(right.code);
+ });
+}
+
+function countBySeverity(findings) {
+ return findings.reduce((counts, item) => {
+ counts[item.severity] = (counts[item.severity] ?? 0) + 1;
+ return counts;
+ }, {});
+}
+
+function countWords(value) {
+ return String(value).trim().split(/\s+/).filter(Boolean).length;
+}
+
+function normalizeAcronym(value) {
+ return String(value).trim().toUpperCase();
+}
+
+function normalizeText(value) {
+ return String(value).trim().toLowerCase().replace(/\s+/g, " ");
+}
+
+function asArray(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function isPlainObject(value) {
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
+}
+
+function escapeRegex(value) {
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
+
+module.exports = {
+ evaluateGlossaryAcronymPacket,
+ renderMarkdownReport,
+ renderSvgSummary
+};
diff --git a/collaborative-glossary-acronym-guard/make-demo-video.js b/collaborative-glossary-acronym-guard/make-demo-video.js
new file mode 100644
index 00000000..ec1ec0ca
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/make-demo-video.js
@@ -0,0 +1,127 @@
+"use strict";
+
+const { execFileSync } = require("node:child_process");
+const fs = require("node:fs");
+const path = require("node:path");
+
+const WIDTH = 960;
+const HEIGHT = 540;
+const FONT = {
+ A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
+ 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"],
+ 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"],
+ X: ["10001", "01010", "00100", "00100", "00100", "01010", "10001"],
+ Y: ["10001", "01010", "00100", "00100", "00100", "00100", "00100"]
+};
+
+const reportsDir = path.join(__dirname, "reports");
+const framesDir = path.join(reportsDir, "frames");
+fs.mkdirSync(framesDir, { recursive: true });
+
+for (const file of fs.readdirSync(framesDir)) {
+ fs.unlinkSync(path.join(framesDir, file));
+}
+
+const slides = [
+ { label: "GLOSSARY GUARD", color: [22, 121, 76], fill: 0.92 },
+ { label: "ACRONYM HOLD", color: [161, 27, 50], fill: 0.42 },
+ { label: "EXPORT READY", color: [22, 121, 76], fill: 0.86 }
+];
+
+let frameIndex = 0;
+for (const slide of slides) {
+ for (let i = 0; i < 8; i += 1) {
+ const progress = (i + 1) / 8;
+ const buffer = createFrame(slide, progress);
+ fs.writeFileSync(path.join(framesDir, `frame-${String(frameIndex).padStart(3, "0")}.ppm`), buffer);
+ frameIndex += 1;
+ }
+}
+
+const output = path.join(reportsDir, "demo.mp4");
+execFileSync(
+ "ffmpeg",
+ [
+ "-y",
+ "-framerate",
+ "8",
+ "-i",
+ path.join(framesDir, "frame-%03d.ppm"),
+ "-pix_fmt",
+ "yuv420p",
+ "-movflags",
+ "+faststart",
+ output
+ ],
+ { stdio: "ignore" }
+);
+
+const stats = fs.statSync(output);
+console.log(`Wrote ${output} (${stats.size} bytes)`);
+
+function createFrame(slide, progress) {
+ const pixels = Buffer.alloc(WIDTH * HEIGHT * 3);
+ fillRect(pixels, 0, 0, WIDTH, HEIGHT, [17, 24, 39]);
+ fillRect(pixels, 48, 48, 864, 444, [248, 250, 252]);
+ fillRect(pixels, 80, 190, 800, 88, [226, 232, 240]);
+ fillRect(pixels, 80, 190, Math.round(800 * slide.fill * progress), 88, slide.color);
+ fillRect(pixels, 80, 322, 240, 42, [226, 232, 240]);
+ fillRect(pixels, 344, 322, 240, 42, [226, 232, 240]);
+ fillRect(pixels, 608, 322, 240, 42, [226, 232, 240]);
+ fillRect(pixels, 80, 322, 60, 42, [161, 27, 50]);
+ fillRect(pixels, 344, 322, 140, 42, [161, 92, 0]);
+ fillRect(pixels, 608, 322, 210, 42, [22, 121, 76]);
+ drawText(pixels, "GLOSSARY GUARD", 82, 104, 5, [17, 24, 39]);
+ drawText(pixels, slide.label, 108, 214, 7, [255, 255, 255]);
+ drawText(pixels, "ACRONYM EXPORT", 82, 414, 4, [51, 65, 85]);
+ return Buffer.concat([Buffer.from(`P6\n${WIDTH} ${HEIGHT}\n255\n`, "ascii"), pixels]);
+}
+
+function fillRect(pixels, x, y, width, height, color) {
+ const x2 = Math.min(WIDTH, x + width);
+ const y2 = Math.min(HEIGHT, y + height);
+ for (let row = Math.max(0, y); row < y2; row += 1) {
+ for (let col = Math.max(0, x); col < x2; col += 1) {
+ const offset = (row * WIDTH + col) * 3;
+ pixels[offset] = color[0];
+ pixels[offset + 1] = color[1];
+ pixels[offset + 2] = color[2];
+ }
+ }
+}
+
+function drawText(pixels, text, x, y, scale, color) {
+ let cursor = x;
+ for (const rawChar of text) {
+ const char = rawChar.toUpperCase();
+ if (char === " ") {
+ cursor += 4 * scale;
+ continue;
+ }
+ const glyph = FONT[char];
+ if (!glyph) {
+ cursor += 6 * scale;
+ continue;
+ }
+ glyph.forEach((row, rowIndex) => {
+ for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
+ if (row[colIndex] === "1") {
+ fillRect(pixels, cursor + colIndex * scale, y + rowIndex * scale, scale, scale, color);
+ }
+ }
+ });
+ cursor += 6 * scale;
+ }
+}
diff --git a/collaborative-glossary-acronym-guard/package.json b/collaborative-glossary-acronym-guard/package.json
new file mode 100644
index 00000000..e917026d
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "collaborative-glossary-acronym-guard",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Synthetic glossary and acronym consistency guard for collaborative scientific manuscripts.",
+ "main": "index.js",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js && node --check make-demo-video.js",
+ "test": "node test.js",
+ "demo": "node demo.js && node make-demo-video.js",
+ "verify-video": "ffprobe -v error -show_entries stream=codec_name,width,height,duration -of default=nokey=1:noprint_wrappers=1 reports/demo.mp4"
+ },
+ "keywords": [
+ "scientific-editor",
+ "glossary",
+ "acronyms",
+ "collaboration",
+ "synthetic"
+ ],
+ "license": "MIT"
+}
diff --git a/collaborative-glossary-acronym-guard/reports/clean-audit.json b/collaborative-glossary-acronym-guard/reports/clean-audit.json
new file mode 100644
index 00000000..b27588ff
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/reports/clean-audit.json
@@ -0,0 +1,35 @@
+{
+ "generatedAt": "2026-06-01T10:00:00.000Z",
+ "status": "READY",
+ "summary": "Glossary, acronym, and terminology export checks passed for the collaborative manuscript.",
+ "findingCounts": {},
+ "findings": [],
+ "exportGlossary": [
+ {
+ "id": "term-crispr",
+ "term": "CRISPR",
+ "acronym": "CRISPR",
+ "expansion": "Clustered Regularly Interspaced Short Palindromic Repeats",
+ "definition": "Genome-editing screen technology used to perturb target genes in pooled experiments.",
+ "discipline": "genomics"
+ },
+ {
+ "id": "term-snr",
+ "term": "SNR",
+ "acronym": "SNR",
+ "expansion": "Signal-to-noise ratio",
+ "definition": "Quantitative ratio comparing measured signal strength against background measurement noise.",
+ "discipline": "imaging"
+ },
+ {
+ "id": "term-batch-effect",
+ "term": "batch effect",
+ "acronym": null,
+ "expansion": null,
+ "definition": "Systematic measurement variation introduced by processing samples in different experimental batches.",
+ "discipline": "statistics"
+ }
+ ],
+ "remediationActions": [],
+ "fingerprint": "08b581cfd1de1ccc"
+}
diff --git a/collaborative-glossary-acronym-guard/reports/demo.mp4 b/collaborative-glossary-acronym-guard/reports/demo.mp4
new file mode 100644
index 00000000..3076d9a6
Binary files /dev/null and b/collaborative-glossary-acronym-guard/reports/demo.mp4 differ
diff --git a/collaborative-glossary-acronym-guard/reports/manifest.json b/collaborative-glossary-acronym-guard/reports/manifest.json
new file mode 100644
index 00000000..902e7a39
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/reports/manifest.json
@@ -0,0 +1,26 @@
+{
+ "module": "collaborative-glossary-acronym-guard",
+ "issue": 12,
+ "generatedAt": "2026-06-01T10:00:00.000Z",
+ "scenarios": [
+ {
+ "name": "clean",
+ "status": "READY",
+ "fingerprint": "08b581cfd1de1ccc",
+ "findings": 0
+ },
+ {
+ "name": "risky",
+ "status": "HOLD",
+ "fingerprint": "2f92e5168084871a",
+ "findings": 15
+ }
+ ],
+ "artifacts": [
+ "reports/clean-audit.json",
+ "reports/risky-audit.json",
+ "reports/risky-review.md",
+ "reports/summary.svg",
+ "reports/demo.mp4"
+ ]
+}
diff --git a/collaborative-glossary-acronym-guard/reports/risky-audit.json b/collaborative-glossary-acronym-guard/reports/risky-audit.json
new file mode 100644
index 00000000..fed5fae1
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/reports/risky-audit.json
@@ -0,0 +1,243 @@
+{
+ "generatedAt": "2026-06-01T10:00:00.000Z",
+ "status": "HOLD",
+ "summary": "Glossary export is hold with 2 critical, 12 high, and 1 warning finding(s).",
+ "findingCounts": {
+ "critical": 2,
+ "high": 12,
+ "warning": 1
+ },
+ "findings": [
+ {
+ "code": "ACRONYM_EXPANSION_COLLISION",
+ "severity": "critical",
+ "message": "SNR has multiple approved expansions: signal-to-noise ratio; single nucleus rna.",
+ "impact": "Readers and downstream exports cannot know which definition applies in each section.",
+ "path": "glossaryEntries.SNR",
+ "remediation": "Split the acronym by discipline or choose one canonical manuscript-level expansion.",
+ "owner": "lead editor"
+ },
+ {
+ "code": "PRIVATE_TERMINOLOGY_NOTE_LEAK",
+ "severity": "critical",
+ "message": "Glossary entry \"SNR\" would leak private reviewer or collaborator terminology notes.",
+ "impact": "Anonymous review notes, local paths, and private comments must not appear in manuscript exports.",
+ "path": "glossaryEntries[1].exportNote",
+ "remediation": "Redact private terminology notes and export only public glossary text.",
+ "owner": "export owner"
+ },
+ {
+ "code": "ACRONYM_FIRST_USE_UNEXPANDED",
+ "severity": "high",
+ "message": "CRISPR first appears in section intro without its expansion.",
+ "impact": "Publication exports should define acronyms at first use for readers and reviewers.",
+ "path": "sections.intro",
+ "remediation": "Introduce \"Clustered Regularly Interspaced Short Palindromic Repeats (CRISPR)\" at first use or move the first mention after the definition.",
+ "owner": "section author"
+ },
+ {
+ "code": "ACRONYM_FIRST_USE_UNEXPANDED",
+ "severity": "high",
+ "message": "SNR first appears in section methods without its expansion.",
+ "impact": "Publication exports should define acronyms at first use for readers and reviewers.",
+ "path": "sections.methods",
+ "remediation": "Introduce \"Signal-to-noise ratio (SNR)\" at first use or move the first mention after the definition.",
+ "owner": "section author"
+ },
+ {
+ "code": "ACRONYM_FIRST_USE_UNEXPANDED",
+ "severity": "high",
+ "message": "SNR first appears in section methods without its expansion.",
+ "impact": "Publication exports should define acronyms at first use for readers and reviewers.",
+ "path": "sections.methods",
+ "remediation": "Introduce \"Single nucleus RNA (SNR)\" at first use or move the first mention after the definition.",
+ "owner": "section author"
+ },
+ {
+ "code": "BLOCKING_TERMINOLOGY_COMMENT",
+ "severity": "high",
+ "message": "Unresolved terminology comment blocks export for batch effect.",
+ "impact": "The collaborative editor should not publish terminology while blocking review threads are open.",
+ "path": "comments[0]",
+ "remediation": "Resolve the terminology comment or mark it non-blocking before export.",
+ "owner": "review owner"
+ },
+ {
+ "code": "GLOSSARY_EXPORT_INCOMPLETE",
+ "severity": "high",
+ "message": "Exportable glossary entry \"SNR\" is missing from the export glossary manifest.",
+ "impact": "Readers will see terminology in the manuscript without a matching glossary definition.",
+ "path": "glossaryEntries[2].exportable",
+ "remediation": "Add the entry id to export.includeGlossaryIds or mark it as internal-only.",
+ "owner": "export owner"
+ },
+ {
+ "code": "GLOSSARY_EXPORT_INCOMPLETE",
+ "severity": "high",
+ "message": "Exportable glossary entry \"batch effect\" is missing from the export glossary manifest.",
+ "impact": "Readers will see terminology in the manuscript without a matching glossary definition.",
+ "path": "glossaryEntries[3].exportable",
+ "remediation": "Add the entry id to export.includeGlossaryIds or mark it as internal-only.",
+ "owner": "export owner"
+ },
+ {
+ "code": "LOCKED_SECTION_TERM_EDIT_UNAPPROVED",
+ "severity": "high",
+ "message": "Terminology edit edit-locked-snr targets locked section methods without current approval.",
+ "impact": "Locked final-review sections should not accept glossary changes without explicit approval.",
+ "path": "proposedEdits[0]",
+ "remediation": "Obtain approval for SNR against section version methods-v8.",
+ "owner": "section owner"
+ },
+ {
+ "code": "MENTIONED_TERM_NOT_EXPORTED",
+ "severity": "high",
+ "message": "\"SNR\" appears in section methods but is absent from the exported glossary.",
+ "impact": "Collaborators can accidentally remove a needed term from the reader-facing glossary.",
+ "path": "sections.methods",
+ "remediation": "Include the mentioned term in the export glossary or remove the stale manuscript mention.",
+ "owner": "export owner"
+ },
+ {
+ "code": "MENTIONED_TERM_NOT_EXPORTED",
+ "severity": "high",
+ "message": "\"batch effect\" appears in section intro but is absent from the exported glossary.",
+ "impact": "Collaborators can accidentally remove a needed term from the reader-facing glossary.",
+ "path": "sections.intro",
+ "remediation": "Include the mentioned term in the export glossary or remove the stale manuscript mention.",
+ "owner": "export owner"
+ },
+ {
+ "code": "TERM_DEFINITION_INCOMPLETE",
+ "severity": "high",
+ "message": "Glossary entry \"SNR\" has an incomplete definition.",
+ "impact": "Short or missing definitions create discipline-specific ambiguity during review.",
+ "path": "glossaryEntries[1].definition",
+ "remediation": "Add a concise, reviewer-ready definition with enough scientific context.",
+ "owner": "terminology editor"
+ },
+ {
+ "code": "TERM_DEFINITION_INCOMPLETE",
+ "severity": "high",
+ "message": "Glossary entry \"batch effect\" has an incomplete definition.",
+ "impact": "Short or missing definitions create discipline-specific ambiguity during review.",
+ "path": "glossaryEntries[3].definition",
+ "remediation": "Add a concise, reviewer-ready definition with enough scientific context.",
+ "owner": "terminology editor"
+ },
+ {
+ "code": "UNAPPROVED_GLOSSARY_TERM",
+ "severity": "high",
+ "message": "Glossary entry \"SNR\" is exportable but not approved.",
+ "impact": "Collaborative manuscripts should not export provisional terminology.",
+ "path": "glossaryEntries[2].status",
+ "remediation": "Route the term through collaborator approval or hold it from export.",
+ "owner": "lead editor"
+ },
+ {
+ "code": "STALE_TERM_SECTION_ANCHOR",
+ "severity": "warning",
+ "message": "Glossary entry \"CRISPR\" was approved against an older version of section intro.",
+ "impact": "Collaborative edits may have changed the term context since approval.",
+ "path": "glossaryEntries[0].sectionVersionHash",
+ "remediation": "Refresh the term anchor against the current section version.",
+ "owner": "section author"
+ }
+ ],
+ "exportGlossary": [
+ {
+ "id": "term-crispr",
+ "term": "CRISPR",
+ "acronym": "CRISPR",
+ "expansion": "Clustered Regularly Interspaced Short Palindromic Repeats",
+ "definition": "Genome editing screen technology used by the manuscript collaborators.",
+ "discipline": "genomics"
+ },
+ {
+ "id": "term-snr-imaging",
+ "term": "SNR",
+ "acronym": "SNR",
+ "expansion": "Signal-to-noise ratio",
+ "definition": "Image metric.",
+ "discipline": "imaging"
+ }
+ ],
+ "remediationActions": [
+ {
+ "code": "ACRONYM_EXPANSION_COLLISION",
+ "owner": "lead editor",
+ "action": "Split the acronym by discipline or choose one canonical manuscript-level expansion."
+ },
+ {
+ "code": "PRIVATE_TERMINOLOGY_NOTE_LEAK",
+ "owner": "export owner",
+ "action": "Redact private terminology notes and export only public glossary text."
+ },
+ {
+ "code": "ACRONYM_FIRST_USE_UNEXPANDED",
+ "owner": "section author",
+ "action": "Introduce \"Clustered Regularly Interspaced Short Palindromic Repeats (CRISPR)\" at first use or move the first mention after the definition."
+ },
+ {
+ "code": "ACRONYM_FIRST_USE_UNEXPANDED",
+ "owner": "section author",
+ "action": "Introduce \"Signal-to-noise ratio (SNR)\" at first use or move the first mention after the definition."
+ },
+ {
+ "code": "ACRONYM_FIRST_USE_UNEXPANDED",
+ "owner": "section author",
+ "action": "Introduce \"Single nucleus RNA (SNR)\" at first use or move the first mention after the definition."
+ },
+ {
+ "code": "BLOCKING_TERMINOLOGY_COMMENT",
+ "owner": "review owner",
+ "action": "Resolve the terminology comment or mark it non-blocking before export."
+ },
+ {
+ "code": "GLOSSARY_EXPORT_INCOMPLETE",
+ "owner": "export owner",
+ "action": "Add the entry id to export.includeGlossaryIds or mark it as internal-only."
+ },
+ {
+ "code": "GLOSSARY_EXPORT_INCOMPLETE",
+ "owner": "export owner",
+ "action": "Add the entry id to export.includeGlossaryIds or mark it as internal-only."
+ },
+ {
+ "code": "LOCKED_SECTION_TERM_EDIT_UNAPPROVED",
+ "owner": "section owner",
+ "action": "Obtain approval for SNR against section version methods-v8."
+ },
+ {
+ "code": "MENTIONED_TERM_NOT_EXPORTED",
+ "owner": "export owner",
+ "action": "Include the mentioned term in the export glossary or remove the stale manuscript mention."
+ },
+ {
+ "code": "MENTIONED_TERM_NOT_EXPORTED",
+ "owner": "export owner",
+ "action": "Include the mentioned term in the export glossary or remove the stale manuscript mention."
+ },
+ {
+ "code": "TERM_DEFINITION_INCOMPLETE",
+ "owner": "terminology editor",
+ "action": "Add a concise, reviewer-ready definition with enough scientific context."
+ },
+ {
+ "code": "TERM_DEFINITION_INCOMPLETE",
+ "owner": "terminology editor",
+ "action": "Add a concise, reviewer-ready definition with enough scientific context."
+ },
+ {
+ "code": "UNAPPROVED_GLOSSARY_TERM",
+ "owner": "lead editor",
+ "action": "Route the term through collaborator approval or hold it from export."
+ },
+ {
+ "code": "STALE_TERM_SECTION_ANCHOR",
+ "owner": "section author",
+ "action": "Refresh the term anchor against the current section version."
+ }
+ ],
+ "fingerprint": "2f92e5168084871a"
+}
diff --git a/collaborative-glossary-acronym-guard/reports/risky-review.md b/collaborative-glossary-acronym-guard/reports/risky-review.md
new file mode 100644
index 00000000..262777a8
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/reports/risky-review.md
@@ -0,0 +1,47 @@
+# Collaborative Glossary and Acronym Guard
+
+Document: Shared editor glossary draft
+Status: HOLD
+Fingerprint: 2f92e5168084871a
+
+## Summary
+
+Glossary export is hold with 2 critical, 12 high, and 1 warning finding(s).
+
+## Findings
+
+- CRITICAL ACRONYM_EXPANSION_COLLISION: SNR has multiple approved expansions: signal-to-noise ratio; single nucleus rna.
+ - Remediation: Split the acronym by discipline or choose one canonical manuscript-level expansion.
+- CRITICAL PRIVATE_TERMINOLOGY_NOTE_LEAK: Glossary entry "SNR" would leak private reviewer or collaborator terminology notes.
+ - Remediation: Redact private terminology notes and export only public glossary text.
+- HIGH ACRONYM_FIRST_USE_UNEXPANDED: CRISPR first appears in section intro without its expansion.
+ - Remediation: Introduce "Clustered Regularly Interspaced Short Palindromic Repeats (CRISPR)" at first use or move the first mention after the definition.
+- HIGH ACRONYM_FIRST_USE_UNEXPANDED: SNR first appears in section methods without its expansion.
+ - Remediation: Introduce "Signal-to-noise ratio (SNR)" at first use or move the first mention after the definition.
+- HIGH ACRONYM_FIRST_USE_UNEXPANDED: SNR first appears in section methods without its expansion.
+ - Remediation: Introduce "Single nucleus RNA (SNR)" at first use or move the first mention after the definition.
+- HIGH BLOCKING_TERMINOLOGY_COMMENT: Unresolved terminology comment blocks export for batch effect.
+ - Remediation: Resolve the terminology comment or mark it non-blocking before export.
+- HIGH GLOSSARY_EXPORT_INCOMPLETE: Exportable glossary entry "SNR" is missing from the export glossary manifest.
+ - Remediation: Add the entry id to export.includeGlossaryIds or mark it as internal-only.
+- HIGH GLOSSARY_EXPORT_INCOMPLETE: Exportable glossary entry "batch effect" is missing from the export glossary manifest.
+ - Remediation: Add the entry id to export.includeGlossaryIds or mark it as internal-only.
+- HIGH LOCKED_SECTION_TERM_EDIT_UNAPPROVED: Terminology edit edit-locked-snr targets locked section methods without current approval.
+ - Remediation: Obtain approval for SNR against section version methods-v8.
+- HIGH MENTIONED_TERM_NOT_EXPORTED: "SNR" appears in section methods but is absent from the exported glossary.
+ - Remediation: Include the mentioned term in the export glossary or remove the stale manuscript mention.
+- HIGH MENTIONED_TERM_NOT_EXPORTED: "batch effect" appears in section intro but is absent from the exported glossary.
+ - Remediation: Include the mentioned term in the export glossary or remove the stale manuscript mention.
+- HIGH TERM_DEFINITION_INCOMPLETE: Glossary entry "SNR" has an incomplete definition.
+ - Remediation: Add a concise, reviewer-ready definition with enough scientific context.
+- HIGH TERM_DEFINITION_INCOMPLETE: Glossary entry "batch effect" has an incomplete definition.
+ - Remediation: Add a concise, reviewer-ready definition with enough scientific context.
+- HIGH UNAPPROVED_GLOSSARY_TERM: Glossary entry "SNR" is exportable but not approved.
+ - Remediation: Route the term through collaborator approval or hold it from export.
+- WARNING STALE_TERM_SECTION_ANCHOR: Glossary entry "CRISPR" was approved against an older version of section intro.
+ - Remediation: Refresh the term anchor against the current section version.
+
+## Export Glossary
+
+- CRISPR - Clustered Regularly Interspaced Short Palindromic Repeats: Genome editing screen technology used by the manuscript collaborators.
+- SNR - Signal-to-noise ratio: Image metric.
diff --git a/collaborative-glossary-acronym-guard/reports/summary.svg b/collaborative-glossary-acronym-guard/reports/summary.svg
new file mode 100644
index 00000000..a2aa781c
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/reports/summary.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/collaborative-glossary-acronym-guard/sample-data.js b/collaborative-glossary-acronym-guard/sample-data.js
new file mode 100644
index 00000000..3e149c68
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/sample-data.js
@@ -0,0 +1,183 @@
+"use strict";
+
+const cleanPacket = {
+ document: {
+ id: "doc-collab-glossary-clean",
+ title: "Collaborative manuscript terminology export",
+ versionHash: "doc-v7-clean"
+ },
+ sections: [
+ {
+ id: "intro",
+ title: "Introduction",
+ versionHash: "intro-v3",
+ locked: false,
+ body:
+ "Clustered Regularly Interspaced Short Palindromic Repeats (CRISPR) screens require a shared terminology layer before collaborator export."
+ },
+ {
+ id: "methods",
+ title: "Methods",
+ versionHash: "methods-v5",
+ locked: true,
+ body:
+ "Signal-to-noise ratio (SNR), batch effect, and unique molecular identifier usage are defined before review."
+ }
+ ],
+ glossaryEntries: [
+ {
+ id: "term-crispr",
+ term: "CRISPR",
+ acronym: "CRISPR",
+ expansion: "Clustered Regularly Interspaced Short Palindromic Repeats",
+ definition: "Genome-editing screen technology used to perturb target genes in pooled experiments.",
+ discipline: "genomics",
+ firstUseSectionId: "intro",
+ sectionVersionHash: "intro-v3",
+ status: "approved",
+ exportable: true,
+ exportNote: "Public glossary definition."
+ },
+ {
+ id: "term-snr",
+ term: "SNR",
+ acronym: "SNR",
+ expansion: "Signal-to-noise ratio",
+ definition: "Quantitative ratio comparing measured signal strength against background measurement noise.",
+ discipline: "imaging",
+ firstUseSectionId: "methods",
+ sectionVersionHash: "methods-v5",
+ status: "approved",
+ exportable: true
+ },
+ {
+ id: "term-batch-effect",
+ term: "batch effect",
+ definition: "Systematic measurement variation introduced by processing samples in different experimental batches.",
+ discipline: "statistics",
+ firstUseSectionId: "methods",
+ sectionVersionHash: "methods-v5",
+ status: "approved",
+ exportable: true
+ }
+ ],
+ proposedEdits: [
+ {
+ id: "edit-approved-batch-definition",
+ termId: "term-batch-effect",
+ sectionId: "methods",
+ sectionVersionHash: "methods-v5",
+ approvedBy: ["lead-author", "stats-reviewer"]
+ }
+ ],
+ comments: [],
+ export: {
+ format: "journal-manuscript",
+ includeGlossaryIds: ["term-crispr", "term-snr", "term-batch-effect"],
+ includePrivateNotes: false
+ }
+};
+
+const riskyPacket = {
+ document: {
+ id: "doc-collab-glossary-risky",
+ title: "Shared editor glossary draft",
+ versionHash: "doc-v11-risky"
+ },
+ sections: [
+ {
+ id: "intro",
+ title: "Introduction",
+ versionHash: "intro-v9",
+ locked: false,
+ body:
+ "CRISPR screens and batch effect corrections were discussed in the shared editor before terminology approval."
+ },
+ {
+ id: "methods",
+ title: "Methods",
+ versionHash: "methods-v8",
+ locked: true,
+ body:
+ "SNR was computed for microscopy while SNR was also used by another collaborator to mean single nucleus RNA."
+ }
+ ],
+ glossaryEntries: [
+ {
+ id: "term-crispr",
+ term: "CRISPR",
+ acronym: "CRISPR",
+ expansion: "Clustered Regularly Interspaced Short Palindromic Repeats",
+ definition: "Genome editing screen technology used by the manuscript collaborators.",
+ discipline: "genomics",
+ firstUseSectionId: "intro",
+ sectionVersionHash: "intro-v7",
+ status: "approved",
+ exportable: true
+ },
+ {
+ id: "term-snr-imaging",
+ term: "SNR",
+ acronym: "SNR",
+ expansion: "Signal-to-noise ratio",
+ definition: "Image metric.",
+ discipline: "imaging",
+ firstUseSectionId: "methods",
+ sectionVersionHash: "methods-v8",
+ status: "approved",
+ exportable: true,
+ privateNote: "Reviewer-only note from anonymous reviewer A: do not export."
+ },
+ {
+ id: "term-snr-rna",
+ term: "SNR",
+ acronym: "SNR",
+ expansion: "Single nucleus RNA",
+ definition: "Sequencing context acronym for single nucleus RNA analysis in draft notes.",
+ discipline: "transcriptomics",
+ firstUseSectionId: "methods",
+ sectionVersionHash: "methods-v8",
+ status: "draft",
+ exportable: true
+ },
+ {
+ id: "term-batch-effect",
+ term: "batch effect",
+ definition: "Batch bias.",
+ discipline: "statistics",
+ firstUseSectionId: "intro",
+ sectionVersionHash: "intro-v9",
+ status: "approved",
+ exportable: true,
+ exportNote: "Public note accidentally includes /Users/reviewer/private terminology draft."
+ }
+ ],
+ proposedEdits: [
+ {
+ id: "edit-locked-snr",
+ termId: "term-snr-imaging",
+ sectionId: "methods",
+ sectionVersionHash: "methods-v7",
+ approvedBy: []
+ }
+ ],
+ comments: [
+ {
+ id: "comment-term-batch-effect",
+ termId: "term-batch-effect",
+ resolved: false,
+ blocksExport: true,
+ body: "Statistics reviewer asked for a clearer discipline-specific definition."
+ }
+ ],
+ export: {
+ format: "journal-manuscript",
+ includeGlossaryIds: ["term-crispr", "term-snr-imaging"],
+ includePrivateNotes: true
+ }
+};
+
+module.exports = {
+ cleanPacket,
+ riskyPacket
+};
diff --git a/collaborative-glossary-acronym-guard/test.js b/collaborative-glossary-acronym-guard/test.js
new file mode 100644
index 00000000..49c2b2f6
--- /dev/null
+++ b/collaborative-glossary-acronym-guard/test.js
@@ -0,0 +1,44 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const {
+ evaluateGlossaryAcronymPacket,
+ renderMarkdownReport,
+ renderSvgSummary
+} = require("./index");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+
+const clean = evaluateGlossaryAcronymPacket(cleanPacket, { now: "2026-06-01T10:00:00.000Z" });
+assert.equal(clean.status, "READY");
+assert.equal(clean.findings.length, 0);
+assert.equal(clean.exportGlossary.length, 3);
+
+const risky = evaluateGlossaryAcronymPacket(riskyPacket, { now: "2026-06-01T10:00:00.000Z" });
+const riskyCodes = new Set(risky.findings.map((finding) => finding.code));
+assert.equal(risky.status, "HOLD");
+assert.ok(riskyCodes.has("ACRONYM_EXPANSION_COLLISION"));
+assert.ok(riskyCodes.has("ACRONYM_FIRST_USE_UNEXPANDED"));
+assert.ok(riskyCodes.has("PRIVATE_TERMINOLOGY_NOTE_LEAK"));
+assert.ok(riskyCodes.has("LOCKED_SECTION_TERM_EDIT_UNAPPROVED"));
+assert.ok(riskyCodes.has("GLOSSARY_EXPORT_INCOMPLETE"));
+assert.ok(riskyCodes.has("TERM_DEFINITION_INCOMPLETE"));
+assert.ok(riskyCodes.has("UNAPPROVED_GLOSSARY_TERM"));
+assert.ok(riskyCodes.has("BLOCKING_TERMINOLOGY_COMMENT"));
+assert.ok(riskyCodes.has("STALE_TERM_SECTION_ANCHOR"));
+
+const repeatedRisky = evaluateGlossaryAcronymPacket(riskyPacket, { now: "2026-06-01T10:05:00.000Z" });
+assert.equal(risky.fingerprint, repeatedRisky.fingerprint);
+
+const markdown = renderMarkdownReport(risky, riskyPacket);
+assert.match(markdown, /Collaborative Glossary and Acronym Guard/);
+assert.match(markdown, /ACRONYM_EXPANSION_COLLISION/);
+assert.match(markdown, /Export Glossary/);
+
+const svg = renderSvgSummary(risky);
+assert.match(svg, /