diff --git a/scientific-terminology-unit-guard/README.md b/scientific-terminology-unit-guard/README.md new file mode 100644 index 00000000..f2e86785 --- /dev/null +++ b/scientific-terminology-unit-guard/README.md @@ -0,0 +1,17 @@ +# Scientific Terminology And Unit Guard + +Self-contained SCIBASE real-time collaborative editor slice for issue #12. The guard checks whether collaborator edits keep scientific terminology, acronyms, units, and equation variables consistent before WYSIWYG, Markdown, LaTeX, or publication exports proceed. + +## Why this slice is distinct + +Existing #12 submissions cover broad editor foundations, operation replay, offline conflict resolution, notebook/kernel collaboration, reference formatting, round-trip export, accessibility parity, suggestion provenance, chat mentions, notification visibility, task dependencies, section locks, and figure/table review lanes. This module focuses only on shared scientific language consistency while multiple collaborators edit the same manuscript. + +## Run + +```bash +npm test +npm run demo +npm run demo:video +``` + +Demo artifacts are written to `reports/`, including JSON, Markdown, SVG, GIF, and MP4 files. diff --git a/scientific-terminology-unit-guard/demo.js b/scientific-terminology-unit-guard/demo.js new file mode 100644 index 00000000..2b8982fa --- /dev/null +++ b/scientific-terminology-unit-guard/demo.js @@ -0,0 +1,59 @@ +const fs = require("fs"); +const path = require("path"); + +const { assessTerminologyAndUnits } = require("./index"); +const { cleanDocument, riskyDocument } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +function markdownReport(name, report) { + const findings = report.findings.length + ? report.findings + .map((item) => `- ${item.severity.toUpperCase()} ${item.code}: ${item.message}`) + .join("\n") + : "- No terminology or unit findings."; + return `# ${report.title} + +Scenario: ${name} + +Decision: ${report.decision.toUpperCase()} + +Reviewed ${report.summary.blocksReviewed} document blocks and ${report.summary.suggestionsReviewed} collaborator suggestions. + +## Findings + +${findings} + +## Release Criteria + +${report.releaseCriteria.map((item) => `- ${item}`).join("\n")} +`; +} + +function svgReport(report) { + const color = report.decision === "hold" ? "#dc2626" : report.decision === "revise" ? "#d97706" : "#16a34a"; + return ` + + Scientific Terminology Unit Guard + ${report.documentId} + + ${report.decision.toUpperCase()} + Findings: ${report.summary.findings} + High: ${report.summary.high} + Medium: ${report.summary.medium} + Low: ${report.summary.low} + Synthetic collaborative manuscript data only. +`; +} + +for (const [name, document] of [ + ["clean-document", cleanDocument], + ["risky-document", riskyDocument], +]) { + const report = assessTerminologyAndUnits(document); + fs.writeFileSync(path.join(reportsDir, `${name}.json`), JSON.stringify(report, null, 2)); + fs.writeFileSync(path.join(reportsDir, `${name}.md`), markdownReport(name, report)); + fs.writeFileSync(path.join(reportsDir, `${name}.svg`), svgReport(report)); + console.log(`${name}: ${report.decision} (${report.summary.findings} findings)`); +} diff --git a/scientific-terminology-unit-guard/demo_video.py b/scientific-terminology-unit-guard/demo_video.py new file mode 100644 index 00000000..109ffcca --- /dev/null +++ b/scientific-terminology-unit-guard/demo_video.py @@ -0,0 +1,46 @@ +from pathlib import Path + +import imageio.v3 as iio +import numpy as np +from PIL import Image, ImageDraw, ImageFont + + +ROOT = Path(__file__).resolve().parent +REPORTS = ROOT / "reports" +REPORTS.mkdir(exist_ok=True) + + +def font(size): + for name in ("arial.ttf", "segoeui.ttf"): + try: + return ImageFont.truetype(name, size) + except OSError: + pass + return ImageFont.load_default() + + +slides = [ + ("Terminology + Unit Guard", "Real-time collaborative editor #12"), + ("Checks", "glossary drift + acronym first-use gaps"), + ("Checks", "unit conflicts + equation variable mismatch"), + ("Decision", "hold publication export until shared language is consistent"), +] + +frames = [] +for index, (title, subtitle) in enumerate(slides, start=1): + image = Image.new("RGB", (960, 544), "#172033") + draw = ImageDraw.Draw(image) + draw.rectangle((46, 54, 914, 490), outline="#a3e635", width=3) + draw.text((82, 124), title, fill="#f8fafc", font=font(42)) + draw.text((82, 206), subtitle, fill="#ecfccb", font=font(26)) + draw.rectangle((82, 326, 742, 382), fill="#365314") + draw.text((104, 342), "collaborator suggestions cannot introduce scientific drift", fill="#f7fee7", font=font(22)) + draw.text((82, 438), f"Slide {index}/4 - synthetic reviewer artifact", fill="#cbd5e1", font=font(20)) + frames.extend([image] * 14) + +gif_path = REPORTS / "demo.gif" +mp4_path = REPORTS / "demo.mp4" +frames[0].save(gif_path, save_all=True, append_images=frames[1:], duration=120, loop=0) +iio.imwrite(mp4_path, [np.asarray(frame) for frame in frames], fps=8, codec="libx264") +print(f"wrote {gif_path}") +print(f"wrote {mp4_path}") diff --git a/scientific-terminology-unit-guard/index.js b/scientific-terminology-unit-guard/index.js new file mode 100644 index 00000000..3e111647 --- /dev/null +++ b/scientific-terminology-unit-guard/index.js @@ -0,0 +1,212 @@ +const HIGH = "high"; +const MEDIUM = "medium"; +const LOW = "low"; + +function requireString(value, field) { + if (typeof value !== "string" || value.trim() === "") { + throw new TypeError(`${field} must be a non-empty string`); + } + return value.trim(); +} + +function list(value, field) { + if (!Array.isArray(value)) { + throw new TypeError(`${field} must be an array`); + } + return value; +} + +function normalizeToken(value) { + return String(value || "").trim(); +} + +function normalizeDocument(raw) { + return { + documentId: requireString(raw.documentId, "documentId"), + title: requireString(raw.title, "title"), + glossary: raw.glossary || {}, + units: raw.units || {}, + variables: raw.variables || {}, + blocks: list(raw.blocks || [], "blocks").map((block) => ({ + id: requireString(block.id, "block.id"), + author: requireString(block.author, "block.author"), + text: String(block.text || ""), + acronyms: list(block.acronyms || [], "block.acronyms"), + units: list(block.units || [], "block.units"), + variables: list(block.variables || [], "block.variables"), + })), + suggestions: list(raw.suggestions || [], "suggestions").map((suggestion) => ({ + id: requireString(suggestion.id, "suggestion.id"), + author: requireString(suggestion.author, "suggestion.author"), + targetBlock: requireString(suggestion.targetBlock, "suggestion.targetBlock"), + text: String(suggestion.text || ""), + acronyms: list(suggestion.acronyms || [], "suggestion.acronyms"), + units: list(suggestion.units || [], "suggestion.units"), + variables: list(suggestion.variables || [], "suggestion.variables"), + })), + }; +} + +function finding(code, severity, source, message, remediation) { + return { + code, + severity, + sourceId: source.id, + author: source.author, + message, + remediation, + }; +} + +function analyzeSource(doc, source, findings) { + for (const item of source.acronyms) { + const short = normalizeToken(item.short); + const expected = normalizeToken(doc.glossary[short]); + const observed = normalizeToken(item.long); + if (!short) { + continue; + } + if (!expected) { + findings.push( + finding( + "UNKNOWN_ACRONYM", + MEDIUM, + source, + `${short} is used but is absent from the shared glossary.`, + "Add the acronym to the shared glossary or replace it before publication export." + ) + ); + } else if (!observed) { + findings.push( + finding( + "MISSING_FIRST_USE_EXPANSION", + MEDIUM, + source, + `${short} appears without its first-use expansion.`, + `Expand ${short} as "${expected}" at first use in the collaborative document.` + ) + ); + } else if (observed.toLowerCase() !== expected.toLowerCase()) { + findings.push( + finding( + "ACRONYM_DRIFT", + HIGH, + source, + `${short} is defined as "${observed}" but glossary expects "${expected}".`, + "Resolve the conflicting expansion in the suggestion or block before accepting changes." + ) + ); + } + } + + for (const item of source.units) { + const quantity = normalizeToken(item.quantity); + const unit = normalizeToken(item.unit); + const expected = normalizeToken(doc.units[quantity]); + if (!expected) { + findings.push( + finding( + "UNKNOWN_QUANTITY_UNIT", + LOW, + source, + `${quantity} has unit "${unit}" but no canonical unit is registered.`, + "Register a canonical unit or mark the quantity as intentionally free-form." + ) + ); + } else if (unit !== expected) { + findings.push( + finding( + "UNIT_CONFLICT", + HIGH, + source, + `${quantity} uses "${unit}" but canonical unit is "${expected}".`, + "Convert or annotate the value before export so collaborators compare like with like." + ) + ); + } + } + + for (const item of source.variables) { + const symbol = normalizeToken(item.symbol); + const meaning = normalizeToken(item.meaning); + const expected = normalizeToken(doc.variables[symbol]); + if (!expected) { + findings.push( + finding( + "UNREGISTERED_VARIABLE", + LOW, + source, + `${symbol} is used in an equation context without a shared definition.`, + "Add the variable to the equation legend before accepting the edit." + ) + ); + } else if (meaning.toLowerCase() !== expected.toLowerCase()) { + findings.push( + finding( + "VARIABLE_MEANING_DRIFT", + HIGH, + source, + `${symbol} means "${meaning}" here but shared definition is "${expected}".`, + "Resolve the variable definition mismatch before rendering equations or exports." + ) + ); + } + } +} + +function assessTerminologyAndUnits(rawDocument) { + const doc = normalizeDocument(rawDocument); + const findings = []; + + for (const block of doc.blocks) { + analyzeSource(doc, block, findings); + } + for (const suggestion of doc.suggestions) { + analyzeSource(doc, suggestion, findings); + } + + const sourceIds = new Set([...doc.blocks, ...doc.suggestions].map((item) => item.id)); + for (const suggestion of doc.suggestions) { + if (!sourceIds.has(suggestion.targetBlock)) { + findings.push( + finding( + "ORPHAN_SUGGESTION_TARGET", + MEDIUM, + suggestion, + `Suggestion targets missing block ${suggestion.targetBlock}.`, + "Retarget or close the suggestion before merging collaborative edits." + ) + ); + } + } + + const high = findings.filter((item) => item.severity === HIGH).length; + const medium = findings.filter((item) => item.severity === MEDIUM).length; + const decision = high > 0 ? "hold" : medium > 0 ? "revise" : "release"; + + return { + documentId: doc.documentId, + title: doc.title, + decision, + summary: { + blocksReviewed: doc.blocks.length, + suggestionsReviewed: doc.suggestions.length, + findings: findings.length, + high, + medium, + low: findings.filter((item) => item.severity === LOW).length, + }, + findings, + releaseCriteria: [ + "Shared glossary acronyms keep one expansion across all blocks and suggestions.", + "Scientific quantities use the canonical unit selected for the collaborative document.", + "Equation variables keep one meaning before WYSIWYG, Markdown, LaTeX, or export render.", + "Suggestions cannot introduce terminology drift while being accepted into the manuscript.", + ], + }; +} + +module.exports = { + assessTerminologyAndUnits, + normalizeDocument, +}; diff --git a/scientific-terminology-unit-guard/package.json b/scientific-terminology-unit-guard/package.json new file mode 100644 index 00000000..eaaf4759 --- /dev/null +++ b/scientific-terminology-unit-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "scientific-terminology-unit-guard", + "version": "1.0.0", + "description": "Collaborative terminology and unit consistency guard for SCIBASE editor issue #12", + "main": "index.js", + "type": "commonjs", + "scripts": { + "test": "node test.js", + "demo": "node demo.js", + "demo:video": "python demo_video.py" + }, + "license": "MIT" +} diff --git a/scientific-terminology-unit-guard/reports/clean-document.json b/scientific-terminology-unit-guard/reports/clean-document.json new file mode 100644 index 00000000..346e8425 --- /dev/null +++ b/scientific-terminology-unit-guard/reports/clean-document.json @@ -0,0 +1,20 @@ +{ + "documentId": "editor-term-001", + "title": "Collaborative catalyst manuscript", + "decision": "release", + "summary": { + "blocksReviewed": 2, + "suggestionsReviewed": 1, + "findings": 0, + "high": 0, + "medium": 0, + "low": 0 + }, + "findings": [], + "releaseCriteria": [ + "Shared glossary acronyms keep one expansion across all blocks and suggestions.", + "Scientific quantities use the canonical unit selected for the collaborative document.", + "Equation variables keep one meaning before WYSIWYG, Markdown, LaTeX, or export render.", + "Suggestions cannot introduce terminology drift while being accepted into the manuscript." + ] +} \ No newline at end of file diff --git a/scientific-terminology-unit-guard/reports/clean-document.md b/scientific-terminology-unit-guard/reports/clean-document.md new file mode 100644 index 00000000..3dc5c2b8 --- /dev/null +++ b/scientific-terminology-unit-guard/reports/clean-document.md @@ -0,0 +1,18 @@ +# Collaborative catalyst manuscript + +Scenario: clean-document + +Decision: RELEASE + +Reviewed 2 document blocks and 1 collaborator suggestions. + +## Findings + +- No terminology or unit findings. + +## Release Criteria + +- Shared glossary acronyms keep one expansion across all blocks and suggestions. +- Scientific quantities use the canonical unit selected for the collaborative document. +- Equation variables keep one meaning before WYSIWYG, Markdown, LaTeX, or export render. +- Suggestions cannot introduce terminology drift while being accepted into the manuscript. diff --git a/scientific-terminology-unit-guard/reports/clean-document.svg b/scientific-terminology-unit-guard/reports/clean-document.svg new file mode 100644 index 00000000..cd96cc57 --- /dev/null +++ b/scientific-terminology-unit-guard/reports/clean-document.svg @@ -0,0 +1,12 @@ + + + Scientific Terminology Unit Guard + editor-term-001 + + RELEASE + Findings: 0 + High: 0 + Medium: 0 + Low: 0 + Synthetic collaborative manuscript data only. + \ No newline at end of file diff --git a/scientific-terminology-unit-guard/reports/demo.gif b/scientific-terminology-unit-guard/reports/demo.gif new file mode 100644 index 00000000..1d224ff8 Binary files /dev/null and b/scientific-terminology-unit-guard/reports/demo.gif differ diff --git a/scientific-terminology-unit-guard/reports/demo.mp4 b/scientific-terminology-unit-guard/reports/demo.mp4 new file mode 100644 index 00000000..5b736d03 Binary files /dev/null and b/scientific-terminology-unit-guard/reports/demo.mp4 differ diff --git a/scientific-terminology-unit-guard/reports/risky-document.json b/scientific-terminology-unit-guard/reports/risky-document.json new file mode 100644 index 00000000..33797da1 --- /dev/null +++ b/scientific-terminology-unit-guard/reports/risky-document.json @@ -0,0 +1,101 @@ +{ + "documentId": "editor-term-002", + "title": "Collaborative catalyst manuscript", + "decision": "hold", + "summary": { + "blocksReviewed": 2, + "suggestionsReviewed": 1, + "findings": 10, + "high": 7, + "medium": 2, + "low": 1 + }, + "findings": [ + { + "code": "MISSING_FIRST_USE_EXPANSION", + "severity": "medium", + "sourceId": "abstract", + "author": "alice", + "message": "HER appears without its first-use expansion.", + "remediation": "Expand HER as \"hydrogen evolution reaction\" at first use in the collaborative document." + }, + { + "code": "MISSING_FIRST_USE_EXPANSION", + "severity": "medium", + "sourceId": "abstract", + "author": "alice", + "message": "FE appears without its first-use expansion.", + "remediation": "Expand FE as \"faradaic efficiency\" at first use in the collaborative document." + }, + { + "code": "UNIT_CONFLICT", + "severity": "high", + "sourceId": "abstract", + "author": "alice", + "message": "temperature uses \"C\" but canonical unit is \"K\".", + "remediation": "Convert or annotate the value before export so collaborators compare like with like." + }, + { + "code": "VARIABLE_MEANING_DRIFT", + "severity": "high", + "sourceId": "abstract", + "author": "alice", + "message": "eta means \"efficiency\" here but shared definition is \"overpotential\".", + "remediation": "Resolve the variable definition mismatch before rendering equations or exports." + }, + { + "code": "ACRONYM_DRIFT", + "severity": "high", + "sourceId": "methods", + "author": "bob", + "message": "RHE is defined as \"rapid heat exchange\" but glossary expects \"reversible hydrogen electrode\".", + "remediation": "Resolve the conflicting expansion in the suggestion or block before accepting changes." + }, + { + "code": "UNIT_CONFLICT", + "severity": "high", + "sourceId": "methods", + "author": "bob", + "message": "currentDensity uses \"A m^-2\" but canonical unit is \"mA cm^-2\".", + "remediation": "Convert or annotate the value before export so collaborators compare like with like." + }, + { + "code": "UNIT_CONFLICT", + "severity": "high", + "sourceId": "methods", + "author": "bob", + "message": "potential uses \"V vs Ag/AgCl\" but canonical unit is \"V vs RHE\".", + "remediation": "Convert or annotate the value before export so collaborators compare like with like." + }, + { + "code": "VARIABLE_MEANING_DRIFT", + "severity": "high", + "sourceId": "methods", + "author": "bob", + "message": "j means \"flux\" here but shared definition is \"current density\".", + "remediation": "Resolve the variable definition mismatch before rendering equations or exports." + }, + { + "code": "UNREGISTERED_VARIABLE", + "severity": "low", + "sourceId": "methods", + "author": "bob", + "message": "E is used in an equation context without a shared definition.", + "remediation": "Add the variable to the equation legend before accepting the edit." + }, + { + "code": "ACRONYM_DRIFT", + "severity": "high", + "sourceId": "sug-risk", + "author": "dana", + "message": "RHE is defined as \"rapid heat exchange\" but glossary expects \"reversible hydrogen electrode\".", + "remediation": "Resolve the conflicting expansion in the suggestion or block before accepting changes." + } + ], + "releaseCriteria": [ + "Shared glossary acronyms keep one expansion across all blocks and suggestions.", + "Scientific quantities use the canonical unit selected for the collaborative document.", + "Equation variables keep one meaning before WYSIWYG, Markdown, LaTeX, or export render.", + "Suggestions cannot introduce terminology drift while being accepted into the manuscript." + ] +} \ No newline at end of file diff --git a/scientific-terminology-unit-guard/reports/risky-document.md b/scientific-terminology-unit-guard/reports/risky-document.md new file mode 100644 index 00000000..94277715 --- /dev/null +++ b/scientific-terminology-unit-guard/reports/risky-document.md @@ -0,0 +1,27 @@ +# Collaborative catalyst manuscript + +Scenario: risky-document + +Decision: HOLD + +Reviewed 2 document blocks and 1 collaborator suggestions. + +## Findings + +- MEDIUM MISSING_FIRST_USE_EXPANSION: HER appears without its first-use expansion. +- MEDIUM MISSING_FIRST_USE_EXPANSION: FE appears without its first-use expansion. +- HIGH UNIT_CONFLICT: temperature uses "C" but canonical unit is "K". +- HIGH VARIABLE_MEANING_DRIFT: eta means "efficiency" here but shared definition is "overpotential". +- HIGH ACRONYM_DRIFT: RHE is defined as "rapid heat exchange" but glossary expects "reversible hydrogen electrode". +- HIGH UNIT_CONFLICT: currentDensity uses "A m^-2" but canonical unit is "mA cm^-2". +- HIGH UNIT_CONFLICT: potential uses "V vs Ag/AgCl" but canonical unit is "V vs RHE". +- HIGH VARIABLE_MEANING_DRIFT: j means "flux" here but shared definition is "current density". +- LOW UNREGISTERED_VARIABLE: E is used in an equation context without a shared definition. +- HIGH ACRONYM_DRIFT: RHE is defined as "rapid heat exchange" but glossary expects "reversible hydrogen electrode". + +## Release Criteria + +- Shared glossary acronyms keep one expansion across all blocks and suggestions. +- Scientific quantities use the canonical unit selected for the collaborative document. +- Equation variables keep one meaning before WYSIWYG, Markdown, LaTeX, or export render. +- Suggestions cannot introduce terminology drift while being accepted into the manuscript. diff --git a/scientific-terminology-unit-guard/reports/risky-document.svg b/scientific-terminology-unit-guard/reports/risky-document.svg new file mode 100644 index 00000000..7cc08d9e --- /dev/null +++ b/scientific-terminology-unit-guard/reports/risky-document.svg @@ -0,0 +1,12 @@ + + + Scientific Terminology Unit Guard + editor-term-002 + + HOLD + Findings: 10 + High: 7 + Medium: 2 + Low: 1 + Synthetic collaborative manuscript data only. + \ No newline at end of file diff --git a/scientific-terminology-unit-guard/requirements-map.md b/scientific-terminology-unit-guard/requirements-map.md new file mode 100644 index 00000000..a7e88e79 --- /dev/null +++ b/scientific-terminology-unit-guard/requirements-map.md @@ -0,0 +1,13 @@ +# Requirements Map + +Issue #12 asks for a real-time collaborative research editor with rich scientific formatting, suggestions/change tracking, version history, publication workflows, and collaborative review. + +| Issue capability | This implementation | +| --- | --- | +| Rich scientific formatting | Validates acronyms, scientific terms, units, and equation variable definitions before Markdown/LaTeX/export rendering. | +| Suggestions and change tracking | Checks collaborator suggestions for glossary drift before they are accepted into the manuscript. | +| Multi-user collaboration | Tracks source block and author on each finding so collaborators can resolve the exact edit. | +| Publication-ready fidelity | Holds export when unit or variable meaning drift would make figures, tables, equations, or text inconsistent. | +| Version/review workflow | Emits deterministic `release`, `revise`, or `hold` decisions with remediation steps for reviewer packets. | + +The module uses synthetic manuscript data only and does not contact credentials, private documents, external APIs, or live editor services. diff --git a/scientific-terminology-unit-guard/sample-data.js b/scientific-terminology-unit-guard/sample-data.js new file mode 100644 index 00000000..2d29edec --- /dev/null +++ b/scientific-terminology-unit-guard/sample-data.js @@ -0,0 +1,99 @@ +const cleanDocument = { + documentId: "editor-term-001", + title: "Collaborative catalyst manuscript", + glossary: { + HER: "hydrogen evolution reaction", + RHE: "reversible hydrogen electrode", + FE: "faradaic efficiency", + }, + units: { + currentDensity: "mA cm^-2", + potential: "V vs RHE", + temperature: "K", + }, + variables: { + eta: "overpotential", + j: "current density", + T: "temperature", + }, + blocks: [ + { + id: "intro", + author: "alice", + text: "The hydrogen evolution reaction (HER) was measured versus the reversible hydrogen electrode (RHE).", + acronyms: [{ short: "HER", long: "hydrogen evolution reaction" }, { short: "RHE", long: "reversible hydrogen electrode" }], + units: [{ quantity: "potential", unit: "V vs RHE" }], + variables: [], + }, + { + id: "results", + author: "bob", + text: "The HER reached 42 mA cm^-2 and 91% faradaic efficiency (FE) at 298 K.", + acronyms: [{ short: "FE", long: "faradaic efficiency" }], + units: [ + { quantity: "currentDensity", unit: "mA cm^-2" }, + { quantity: "temperature", unit: "K" }, + ], + variables: [ + { symbol: "eta", meaning: "overpotential" }, + { symbol: "j", meaning: "current density" }, + ], + }, + ], + suggestions: [ + { + id: "sug-1", + author: "carol", + targetBlock: "results", + text: "Clarify that eta is the overpotential used for Tafel analysis.", + acronyms: [], + units: [], + variables: [{ symbol: "eta", meaning: "overpotential" }], + }, + ], +}; + +const riskyDocument = { + ...cleanDocument, + documentId: "editor-term-002", + blocks: [ + { + id: "abstract", + author: "alice", + text: "HER improves at 25 C with strong FE, but no first-use expansions are provided.", + acronyms: [{ short: "HER", long: "" }, { short: "FE", long: "" }], + units: [{ quantity: "temperature", unit: "C" }], + variables: [{ symbol: "eta", meaning: "efficiency" }], + }, + { + id: "methods", + author: "bob", + text: "Current density j was normalized as A/m2, and E was reported versus Ag/AgCl.", + acronyms: [{ short: "RHE", long: "rapid heat exchange" }], + units: [ + { quantity: "currentDensity", unit: "A m^-2" }, + { quantity: "potential", unit: "V vs Ag/AgCl" }, + ], + variables: [ + { symbol: "j", meaning: "flux" }, + { symbol: "E", meaning: "electrode potential" }, + ], + }, + ], + suggestions: [ + { + id: "sug-risk", + author: "dana", + targetBlock: "methods", + text: "Rename RHE to rapid heat exchange throughout.", + acronyms: [{ short: "RHE", long: "rapid heat exchange" }], + units: [], + variables: [], + }, + ], +}; + +module.exports = { + cleanDocument, + riskyDocument, +}; diff --git a/scientific-terminology-unit-guard/test.js b/scientific-terminology-unit-guard/test.js new file mode 100644 index 00000000..60accca2 --- /dev/null +++ b/scientific-terminology-unit-guard/test.js @@ -0,0 +1,45 @@ +const assert = require("assert"); + +const { assessTerminologyAndUnits, normalizeDocument } = require("./index"); +const { cleanDocument, riskyDocument } = require("./sample-data"); + +const clean = assessTerminologyAndUnits(cleanDocument); +assert.strictEqual(clean.decision, "release"); +assert.strictEqual(clean.summary.findings, 0); + +const risky = assessTerminologyAndUnits(riskyDocument); +assert.strictEqual(risky.decision, "hold"); +for (const code of [ + "MISSING_FIRST_USE_EXPANSION", + "ACRONYM_DRIFT", + "UNIT_CONFLICT", + "VARIABLE_MEANING_DRIFT", + "UNREGISTERED_VARIABLE", +]) { + assert(risky.findings.some((finding) => finding.code === code), `missing ${code}`); +} + +const unknown = assessTerminologyAndUnits({ + ...cleanDocument, + blocks: [ + { + id: "new-block", + author: "eve", + text: "New metric ABC is introduced.", + acronyms: [{ short: "ABC", long: "adaptive binding coefficient" }], + units: [{ quantity: "bindingRate", unit: "s^-1" }], + variables: [], + }, + ], + suggestions: [], +}); +assert.strictEqual(unknown.decision, "revise"); +assert(unknown.findings.some((finding) => finding.code === "UNKNOWN_ACRONYM")); +assert(unknown.findings.some((finding) => finding.code === "UNKNOWN_QUANTITY_UNIT")); + +assert.throws( + () => normalizeDocument({ ...cleanDocument, documentId: "" }), + /documentId must be a non-empty string/ +); + +console.log("scientific terminology and unit guard tests passed");