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 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 @@ + + + +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.");