From 755c341788f7a7ccb98246b28affe72d63e46fc9 Mon Sep 17 00:00:00 2001 From: AlonePenguin <187998801+AlonePenguin@users.noreply.github.com> Date: Mon, 1 Jun 2026 05:54:01 -0400 Subject: [PATCH] Add collaborative glossary acronym guard --- .../.gitignore | 1 + .../README.md | 42 ++ collaborative-glossary-acronym-guard/demo.js | 52 ++ collaborative-glossary-acronym-guard/index.js | 515 ++++++++++++++++++ .../make-demo-video.js | 127 +++++ .../package.json | 21 + .../reports/clean-audit.json | 35 ++ .../reports/demo.mp4 | Bin 0 -> 20928 bytes .../reports/manifest.json | 26 + .../reports/risky-audit.json | 243 +++++++++ .../reports/risky-review.md | 47 ++ .../reports/summary.svg | 13 + .../sample-data.js | 183 +++++++ collaborative-glossary-acronym-guard/test.js | 44 ++ 14 files changed, 1349 insertions(+) create mode 100644 collaborative-glossary-acronym-guard/.gitignore create mode 100644 collaborative-glossary-acronym-guard/README.md create mode 100644 collaborative-glossary-acronym-guard/demo.js create mode 100644 collaborative-glossary-acronym-guard/index.js create mode 100644 collaborative-glossary-acronym-guard/make-demo-video.js create mode 100644 collaborative-glossary-acronym-guard/package.json create mode 100644 collaborative-glossary-acronym-guard/reports/clean-audit.json create mode 100644 collaborative-glossary-acronym-guard/reports/demo.mp4 create mode 100644 collaborative-glossary-acronym-guard/reports/manifest.json create mode 100644 collaborative-glossary-acronym-guard/reports/risky-audit.json create mode 100644 collaborative-glossary-acronym-guard/reports/risky-review.md create mode 100644 collaborative-glossary-acronym-guard/reports/summary.svg create mode 100644 collaborative-glossary-acronym-guard/sample-data.js create mode 100644 collaborative-glossary-acronym-guard/test.js 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 [ + ``, + ``, + ``, + `Collaborative glossary guard`, + `Status ${escapeXml(result.status)} - fingerprint ${escapeXml(result.fingerprint)}`, + ``, + ``, + ``, + `READY ${counts.info ?? 0}`, + `Critical/high blockers: ${critical + high}`, + `Warnings: ${warning}`, + `Export glossary entries: ${result.exportGlossary.length}`, + `` + ].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 0000000000000000000000000000000000000000..3076d9a691211bd8dc5f3a08eb63e7b76bb258c3 GIT binary patch literal 20928 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 literal 0 HcmV?d00001 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 @@ + + + +Collaborative glossary guard +Status HOLD - fingerprint 2f92e5168084871a + + + +READY 0 +Critical/high blockers: 14 +Warnings: 1 +Export glossary entries: 2 + \ 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, / evaluateGlossaryAcronymPacket(null), /expects a packet object/); + +console.log("All glossary/acronym guard tests passed.");