diff --git a/manuscript-terminology-definition-assistant/README.md b/manuscript-terminology-definition-assistant/README.md
new file mode 100644
index 00000000..8d87788f
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/README.md
@@ -0,0 +1,37 @@
+# Manuscript Terminology Definition Assistant
+
+This module is a focused AI-Assisted Research Tools slice for SCIBASE issue #13. It reviews manuscript terminology, acronym expansion, nomenclature style, citation term bindings, and lay-summary jargon before AI-generated peer-review packets, summaries, or citation recommendations are released.
+
+The assistant checks:
+
+- acronym expansion at first use, with stricter handling in title, abstract, summary, and lay-summary sections
+- conflicting acronym expansions
+- required reviewer-facing definitions for technical terms
+- missing lay definitions for public-facing summaries
+- preferred nomenclature and domain style drift
+- citation recommendations that reference unknown or undefined terms
+- generated summaries that include unexplained high-jargon terms
+
+It is intentionally separate from broad research-tool suites, evidence-grounded summarizers, citation context or metadata guards, statistical review, protocol deviation review, lay-summary safety, and collaborative editor glossary/export checks. This slice focuses on terminology readiness inside AI peer-review and summary packets.
+
+## Reviewer Path
+
+```bash
+npm run check
+npm test
+npm run demo
+npm run verify-video
+```
+
+Generated reviewer artifacts:
+
+- `reports/clean-terminology-packet.json`
+- `reports/risky-terminology-packet.json`
+- `reports/terminology-review-report.md`
+- `reports/summary.svg`
+- `reports/demo-script.txt`
+- `reports/demo.mp4`
+
+## Safety
+
+All fixtures are synthetic. The module does not call uploaded manuscript stores, private corpora, citation indexes, external AI APIs, credential stores, payment systems, or external services.
diff --git a/manuscript-terminology-definition-assistant/demo.js b/manuscript-terminology-definition-assistant/demo.js
new file mode 100644
index 00000000..4e58003b
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/demo.js
@@ -0,0 +1,50 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const { evaluateTerminologyPacket, renderMarkdownReport, renderSvgSummary } = require("./index");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const cleanEvaluation = evaluateTerminologyPacket(cleanPacket);
+const riskyEvaluation = evaluateTerminologyPacket(riskyPacket);
+
+fs.writeFileSync(
+ path.join(reportsDir, "clean-terminology-packet.json"),
+ `${JSON.stringify({ input: cleanPacket, evaluation: cleanEvaluation }, null, 2)}\n`
+);
+fs.writeFileSync(
+ path.join(reportsDir, "risky-terminology-packet.json"),
+ `${JSON.stringify({ input: riskyPacket, evaluation: riskyEvaluation }, null, 2)}\n`
+);
+fs.writeFileSync(
+ path.join(reportsDir, "terminology-review-report.md"),
+ renderMarkdownReport(riskyPacket, riskyEvaluation)
+);
+fs.writeFileSync(
+ path.join(reportsDir, "summary.svg"),
+ renderSvgSummary(riskyEvaluation)
+);
+fs.writeFileSync(
+ path.join(reportsDir, "demo-script.txt"),
+ [
+ "Manuscript terminology definition assistant demo",
+ "",
+ `Clean packet decision: ${cleanEvaluation.summary.decision}`,
+ `Clean audit digest: ${cleanEvaluation.summary.auditDigest}`,
+ "",
+ `Risky packet decision: ${riskyEvaluation.summary.decision}`,
+ `Risky finding count: ${riskyEvaluation.summary.findingCount}`,
+ `Risky audit digest: ${riskyEvaluation.summary.auditDigest}`,
+ "",
+ "The risky packet demonstrates acronym expansion conflicts, missing first-use definitions, unknown citation term bindings, nomenclature style drift, and unexplained lay-summary jargon.",
+ ""
+ ].join("\n")
+);
+
+console.log(JSON.stringify({
+ cleanDecision: cleanEvaluation.summary.decision,
+ riskyDecision: riskyEvaluation.summary.decision,
+ riskyFindings: riskyEvaluation.summary.findingCount,
+ report: "reports/terminology-review-report.md"
+}, null, 2));
diff --git a/manuscript-terminology-definition-assistant/index.js b/manuscript-terminology-definition-assistant/index.js
new file mode 100644
index 00000000..f1a58874
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/index.js
@@ -0,0 +1,305 @@
+const crypto = require("node:crypto");
+
+const ABSTRACT_SECTIONS = new Set(["title", "abstract", "summary", "lay_summary"]);
+
+function asArray(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function stableJson(value) {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableJson).join(",")}]`;
+ }
+ if (value && typeof value === "object") {
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
+ }
+ return JSON.stringify(value);
+}
+
+function sha256(value) {
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
+}
+
+function normalize(value) {
+ return String(value || "").trim().toLowerCase().replace(/\s+/g, " ");
+}
+
+function severityRank(severity) {
+ return { critical: 4, high: 3, medium: 2, low: 1 }[severity] || 0;
+}
+
+function addFinding(findings, severity, code, message, refs, action) {
+ findings.push({
+ severity,
+ code,
+ message,
+ refs: asArray(refs),
+ action
+ });
+}
+
+function evaluateTerminologyPacket(packet) {
+ const findings = [];
+ const terms = asArray(packet.terms);
+ const citations = asArray(packet.citationRecommendations);
+ const summaries = asArray(packet.generatedSummaries);
+ const termById = new Map(terms.map((term) => [term.id, term]));
+ const acronymGroups = new Map();
+
+ for (const term of terms) {
+ if (!term.id) {
+ addFinding(findings, "high", "TERM_MISSING_ID", "A terminology entry is missing a stable id.", [], "assign_term_id");
+ continue;
+ }
+
+ const short = String(term.short || "").trim();
+ if (short) {
+ if (!acronymGroups.has(short.toUpperCase())) {
+ acronymGroups.set(short.toUpperCase(), []);
+ }
+ acronymGroups.get(short.toUpperCase()).push(term);
+ }
+
+ if (short && term.expandedAtFirstUse !== true) {
+ const severity = ABSTRACT_SECTIONS.has(term.firstUseSection) ? "high" : "medium";
+ addFinding(
+ findings,
+ severity,
+ "ACRONYM_NOT_EXPANDED_AT_FIRST_USE",
+ `${short} is not expanded at first use in ${term.firstUseSection || "an unknown section"}.`,
+ [term.id],
+ "expand_acronym_at_first_use"
+ );
+ }
+
+ if (term.requiresDefinition && !String(term.definition || "").trim()) {
+ addFinding(
+ findings,
+ ABSTRACT_SECTIONS.has(term.firstUseSection) ? "high" : "medium",
+ "TERM_DEFINITION_MISSING",
+ `${term.label || term.id} requires a reviewer-facing definition before AI summaries or citation suggestions use it.`,
+ [term.id],
+ "add_reviewer_facing_definition"
+ );
+ }
+
+ if (term.audience === "lay" && term.jargonLevel === "high" && !term.layDefinition) {
+ addFinding(
+ findings,
+ "medium",
+ "LAY_DEFINITION_MISSING",
+ `${term.label || term.id} appears in a lay-facing packet without a lay definition.`,
+ [term.id],
+ "add_lay_definition_or_remove_from_lay_summary"
+ );
+ }
+
+ const observedForms = new Set(asArray(term.observedForms).map(normalize).filter(Boolean));
+ const preferred = normalize(term.preferredForm || term.label);
+ if (preferred && observedForms.size > 1 && !observedForms.has(preferred)) {
+ addFinding(
+ findings,
+ "medium",
+ "PREFERRED_TERM_FORM_ABSENT",
+ `${term.label || term.id} has multiple observed forms but none match the preferred nomenclature.`,
+ [term.id],
+ "normalize_term_to_preferred_form"
+ );
+ }
+
+ if (term.expectedStyle && term.observedStyle && term.expectedStyle !== term.observedStyle) {
+ addFinding(
+ findings,
+ "medium",
+ "NOMENCLATURE_STYLE_MISMATCH",
+ `${term.label || term.id} uses ${term.observedStyle} style where ${term.expectedStyle} style is expected.`,
+ [term.id],
+ "fix_domain_nomenclature_style"
+ );
+ }
+ }
+
+ for (const [short, group] of acronymGroups.entries()) {
+ const expansions = new Set(group.map((term) => normalize(term.expansion)).filter(Boolean));
+ if (expansions.size > 1) {
+ addFinding(
+ findings,
+ "high",
+ "ACRONYM_EXPANSION_CONFLICT",
+ `${short} maps to ${expansions.size} different expansions in the manuscript packet.`,
+ group.map((term) => term.id),
+ "resolve_acronym_expansion_conflict"
+ );
+ }
+ }
+
+ for (const citation of citations) {
+ const term = termById.get(citation.termId);
+ if (!term) {
+ addFinding(
+ findings,
+ "medium",
+ "CITATION_TERM_UNKNOWN",
+ `Citation recommendation ${citation.id || "unknown"} references an unknown terminology id.`,
+ [citation.id],
+ "bind_citation_to_known_term"
+ );
+ continue;
+ }
+ if (term.requiresDefinition && !String(term.definition || "").trim()) {
+ addFinding(
+ findings,
+ "medium",
+ "CITATION_BEFORE_TERM_DEFINITION",
+ `Citation recommendation ${citation.id || "unknown"} uses ${term.label || term.id} before the term is defined.`,
+ [citation.id, term.id],
+ "define_term_before_citation_insertion"
+ );
+ }
+ }
+
+ for (const summary of summaries) {
+ for (const termId of asArray(summary.termIds)) {
+ const term = termById.get(termId);
+ if (!term) {
+ continue;
+ }
+ if (summary.mode === "layperson" && (term.jargonLevel === "high" || term.audience === "expert") && !term.layDefinition) {
+ addFinding(
+ findings,
+ "high",
+ "LAY_SUMMARY_JARGON_UNEXPLAINED",
+ `Generated layperson summary ${summary.id || "unknown"} includes ${term.label || term.id} without an accessible definition.`,
+ [summary.id, term.id],
+ "rewrite_lay_summary_with_definition"
+ );
+ }
+ }
+ }
+
+ findings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity) || a.code.localeCompare(b.code));
+ const decision = findings.some((finding) => severityRank(finding.severity) >= 3)
+ ? "hold_ai_research_packet"
+ : findings.some((finding) => finding.severity === "medium")
+ ? "revise_terminology_packet"
+ : "release_ai_research_packet";
+
+ const coverage = terms.map((term) => ({
+ id: term.id,
+ label: term.label,
+ short: term.short || null,
+ firstUseSection: term.firstUseSection || null,
+ hasDefinition: Boolean(String(term.definition || "").trim()),
+ hasLayDefinition: Boolean(term.layDefinition),
+ observedForms: asArray(term.observedForms)
+ }));
+
+ const summary = {
+ manuscriptId: packet.manuscriptId,
+ decision,
+ termsReviewed: terms.length,
+ citationRecommendationsReviewed: citations.length,
+ generatedSummariesReviewed: summaries.length,
+ findingCount: findings.length,
+ highOrCriticalFindings: findings.filter((finding) => severityRank(finding.severity) >= 3).length
+ };
+ const auditDigest = `sha256:${sha256({ summary, findings, coverage }).slice(0, 16)}`;
+
+ return {
+ summary: {
+ ...summary,
+ auditDigest
+ },
+ coverage,
+ findings,
+ actions: buildActions(findings)
+ };
+}
+
+function buildActions(findings) {
+ const seen = new Set();
+ const actions = [];
+ for (const finding of findings) {
+ if (!finding.action || seen.has(finding.action)) {
+ continue;
+ }
+ seen.add(finding.action);
+ actions.push({
+ id: finding.action,
+ severity: finding.severity,
+ refs: finding.refs
+ });
+ }
+ return actions;
+}
+
+function renderMarkdownReport(packet, evaluation) {
+ const lines = [];
+ lines.push(`# Manuscript Terminology Definition Review: ${packet.manuscriptId}`);
+ lines.push("");
+ lines.push(`Decision: **${evaluation.summary.decision}**`);
+ lines.push(`Audit digest: \`${evaluation.summary.auditDigest}\``);
+ lines.push("");
+ lines.push("## Findings");
+ lines.push("");
+ if (evaluation.findings.length === 0) {
+ lines.push("No terminology blockers were detected.");
+ } else {
+ lines.push("| Severity | Code | Message | Action |");
+ lines.push("| --- | --- | --- | --- |");
+ for (const finding of evaluation.findings) {
+ lines.push(`| ${finding.severity} | \`${finding.code}\` | ${escapeMarkdown(finding.message)} | \`${finding.action}\` |`);
+ }
+ }
+ lines.push("");
+ lines.push("## Term Coverage");
+ lines.push("");
+ lines.push("| Term | Short | Section | Definition | Lay definition | Observed forms |");
+ lines.push("| --- | --- | --- | --- | --- | --- |");
+ for (const item of evaluation.coverage) {
+ lines.push(`| ${item.label || item.id} | ${item.short || ""} | ${item.firstUseSection || ""} | ${item.hasDefinition ? "yes" : "no"} | ${item.hasLayDefinition ? "yes" : "no"} | ${item.observedForms.join(", ")} |`);
+ }
+ lines.push("");
+ lines.push("Synthetic data only. No uploaded manuscripts, private corpora, citation indexes, external AI APIs, credentials, or payment systems are used.");
+ return `${lines.join("\n")}\n`;
+}
+
+function renderSvgSummary(evaluation) {
+ const color = evaluation.summary.decision === "hold_ai_research_packet" ? "#b91c1c" : evaluation.summary.decision === "revise_terminology_packet" ? "#b45309" : "#047857";
+ const rows = evaluation.findings.slice(0, 5).map((finding, index) => {
+ const y = 304 + index * 42;
+ return `${escapeXml(finding.severity.toUpperCase())} ${escapeXml(finding.code)}`;
+ }).join("\n");
+ return `
+
+`;
+}
+
+function escapeMarkdown(value) {
+ return String(value).replace(/\|/g, "\\|").replace(/\n/g, " ");
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+module.exports = {
+ evaluateTerminologyPacket,
+ renderMarkdownReport,
+ renderSvgSummary,
+ sha256
+};
diff --git a/manuscript-terminology-definition-assistant/make-demo-video.js b/manuscript-terminology-definition-assistant/make-demo-video.js
new file mode 100644
index 00000000..571e994c
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/make-demo-video.js
@@ -0,0 +1,88 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const { spawnSync } = require("node:child_process");
+const { evaluateTerminologyPacket } = require("./index");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+const framesDir = path.join(reportsDir, "frames");
+fs.mkdirSync(framesDir, { recursive: true });
+
+const clean = evaluateTerminologyPacket(cleanPacket);
+const risky = evaluateTerminologyPacket(riskyPacket);
+const width = 960;
+const height = 540;
+const frames = 72;
+const fps = 18;
+
+function setPixel(buffer, x, y, r, g, b) {
+ if (x < 0 || y < 0 || x >= width || y >= height) {
+ return;
+ }
+ const offset = (y * width + x) * 3;
+ buffer[offset] = r;
+ buffer[offset + 1] = g;
+ buffer[offset + 2] = b;
+}
+
+function fillRect(buffer, x, y, w, h, r, g, b) {
+ for (let row = y; row < y + h; row += 1) {
+ for (let col = x; col < x + w; col += 1) {
+ setPixel(buffer, col, row, r, g, b);
+ }
+ }
+}
+
+function writeFrame(index, progress) {
+ const buffer = Buffer.alloc(width * height * 3, 248);
+ fillRect(buffer, 0, 0, width, height, 248, 250, 252);
+ fillRect(buffer, 54, 48, 852, 444, 255, 255, 255);
+ fillRect(buffer, 54, 48, 852, 8, 17, 24, 39);
+
+ const left = Math.floor(330 * Math.min(1, progress * 1.8));
+ const right = Math.floor(330 * Math.max(0, (progress - 0.3) * 1.55));
+ fillRect(buffer, 104, 112, 330, 78, 229, 231, 235);
+ fillRect(buffer, 104, 112, left, 78, 5, 150, 105);
+ fillRect(buffer, 526, 112, 330, 78, 229, 231, 235);
+ fillRect(buffer, 526, 112, right, 78, 185, 28, 28);
+
+ for (let i = 0; i < clean.summary.termsReviewed; i += 1) {
+ fillRect(buffer, 116 + i * 58, 246, 40, 124, 16, 185, 129);
+ }
+
+ for (let i = 0; i < Math.min(9, risky.summary.findingCount); i += 1) {
+ const barHeight = 38 + (i % 4) * 26;
+ fillRect(buffer, 548 + i * 30, 378 - barHeight, 22, barHeight, 220, 38, 38);
+ }
+
+ fillRect(buffer, 104, 430, Math.floor(752 * progress), 18, 37, 99, 235);
+ const header = Buffer.from(`P6\n${width} ${height}\n255\n`, "ascii");
+ fs.writeFileSync(path.join(framesDir, `frame-${String(index).padStart(3, "0")}.ppm`), Buffer.concat([header, buffer]));
+}
+
+for (let index = 0; index < frames; index += 1) {
+ writeFrame(index, index / (frames - 1));
+}
+
+const output = path.join(reportsDir, "demo.mp4");
+const ffmpeg = process.env.FFMPEG_PATH || "ffmpeg";
+const result = spawnSync(ffmpeg, [
+ "-y",
+ "-framerate",
+ String(fps),
+ "-i",
+ path.join(framesDir, "frame-%03d.ppm"),
+ "-pix_fmt",
+ "yuv420p",
+ "-movflags",
+ "+faststart",
+ output
+], { stdio: "inherit" });
+
+fs.rmSync(framesDir, { recursive: true, force: true });
+
+if (result.status !== 0) {
+ process.exit(result.status || 1);
+}
+
+console.log(`Wrote ${output}`);
diff --git a/manuscript-terminology-definition-assistant/package.json b/manuscript-terminology-definition-assistant/package.json
new file mode 100644
index 00000000..f8c72cb0
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "manuscript-terminology-definition-assistant",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Synthetic manuscript terminology and acronym definition assistant for SCIBASE AI research tools.",
+ "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 -select_streams v:0 -show_entries stream=codec_name,width,height,duration,avg_frame_rate -show_entries format=duration,size -of default=noprint_wrappers=1 reports/demo.mp4"
+ },
+ "keywords": [
+ "scibase",
+ "ai-assisted-research",
+ "terminology",
+ "acronyms",
+ "peer-review"
+ ],
+ "license": "MIT"
+}
diff --git a/manuscript-terminology-definition-assistant/reports/clean-terminology-packet.json b/manuscript-terminology-definition-assistant/reports/clean-terminology-packet.json
new file mode 100644
index 00000000..1a8214bf
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/reports/clean-terminology-packet.json
@@ -0,0 +1,123 @@
+{
+ "input": {
+ "manuscriptId": "astrocyte-calcium-preprint-v3",
+ "terms": [
+ {
+ "id": "term-gcamp",
+ "label": "genetically encoded calcium indicator",
+ "short": "GECI",
+ "expansion": "genetically encoded calcium indicator",
+ "firstUseSection": "abstract",
+ "expandedAtFirstUse": true,
+ "requiresDefinition": true,
+ "definition": "A fluorescent protein sensor used to report intracellular calcium dynamics.",
+ "layDefinition": "A lab-made sensor that glows when cell calcium changes.",
+ "jargonLevel": "medium",
+ "observedForms": [
+ "genetically encoded calcium indicator",
+ "GECI"
+ ],
+ "preferredForm": "genetically encoded calcium indicator"
+ },
+ {
+ "id": "term-gfap",
+ "label": "glial fibrillary acidic protein",
+ "short": "GFAP",
+ "expansion": "glial fibrillary acidic protein",
+ "firstUseSection": "methods",
+ "expandedAtFirstUse": true,
+ "requiresDefinition": true,
+ "definition": "An astrocyte marker used to identify glial cells in the imaging cohort.",
+ "layDefinition": "A marker that helps identify support cells in the brain.",
+ "jargonLevel": "medium",
+ "observedForms": [
+ "glial fibrillary acidic protein",
+ "GFAP"
+ ],
+ "preferredForm": "glial fibrillary acidic protein"
+ },
+ {
+ "id": "term-stat-model",
+ "label": "mixed-effects model",
+ "firstUseSection": "results",
+ "expandedAtFirstUse": true,
+ "requiresDefinition": true,
+ "definition": "A statistical model that accounts for repeated observations from the same animal.",
+ "layDefinition": "A model that avoids counting repeated measurements as unrelated.",
+ "jargonLevel": "medium",
+ "observedForms": [
+ "mixed-effects model"
+ ],
+ "preferredForm": "mixed-effects model"
+ }
+ ],
+ "citationRecommendations": [
+ {
+ "id": "cite-geci-method",
+ "termId": "term-gcamp",
+ "reason": "Supports calcium indicator methodology."
+ }
+ ],
+ "generatedSummaries": [
+ {
+ "id": "summary-lay",
+ "mode": "layperson",
+ "termIds": [
+ "term-gcamp",
+ "term-stat-model"
+ ]
+ }
+ ]
+ },
+ "evaluation": {
+ "summary": {
+ "manuscriptId": "astrocyte-calcium-preprint-v3",
+ "decision": "release_ai_research_packet",
+ "termsReviewed": 3,
+ "citationRecommendationsReviewed": 1,
+ "generatedSummariesReviewed": 1,
+ "findingCount": 0,
+ "highOrCriticalFindings": 0,
+ "auditDigest": "sha256:c0ce9eb80921f467"
+ },
+ "coverage": [
+ {
+ "id": "term-gcamp",
+ "label": "genetically encoded calcium indicator",
+ "short": "GECI",
+ "firstUseSection": "abstract",
+ "hasDefinition": true,
+ "hasLayDefinition": true,
+ "observedForms": [
+ "genetically encoded calcium indicator",
+ "GECI"
+ ]
+ },
+ {
+ "id": "term-gfap",
+ "label": "glial fibrillary acidic protein",
+ "short": "GFAP",
+ "firstUseSection": "methods",
+ "hasDefinition": true,
+ "hasLayDefinition": true,
+ "observedForms": [
+ "glial fibrillary acidic protein",
+ "GFAP"
+ ]
+ },
+ {
+ "id": "term-stat-model",
+ "label": "mixed-effects model",
+ "short": null,
+ "firstUseSection": "results",
+ "hasDefinition": true,
+ "hasLayDefinition": true,
+ "observedForms": [
+ "mixed-effects model"
+ ]
+ }
+ ],
+ "findings": [],
+ "actions": []
+ }
+}
diff --git a/manuscript-terminology-definition-assistant/reports/demo-script.txt b/manuscript-terminology-definition-assistant/reports/demo-script.txt
new file mode 100644
index 00000000..6a90dfab
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/reports/demo-script.txt
@@ -0,0 +1,10 @@
+Manuscript terminology definition assistant demo
+
+Clean packet decision: release_ai_research_packet
+Clean audit digest: sha256:c0ce9eb80921f467
+
+Risky packet decision: hold_ai_research_packet
+Risky finding count: 12
+Risky audit digest: sha256:525040b780da7400
+
+The risky packet demonstrates acronym expansion conflicts, missing first-use definitions, unknown citation term bindings, nomenclature style drift, and unexplained lay-summary jargon.
diff --git a/manuscript-terminology-definition-assistant/reports/demo.mp4 b/manuscript-terminology-definition-assistant/reports/demo.mp4
new file mode 100644
index 00000000..667a4840
Binary files /dev/null and b/manuscript-terminology-definition-assistant/reports/demo.mp4 differ
diff --git a/manuscript-terminology-definition-assistant/reports/risky-terminology-packet.json b/manuscript-terminology-definition-assistant/reports/risky-terminology-packet.json
new file mode 100644
index 00000000..88d6e479
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/reports/risky-terminology-packet.json
@@ -0,0 +1,339 @@
+{
+ "input": {
+ "manuscriptId": "oncology-organoid-draft-v1",
+ "terms": [
+ {
+ "id": "term-pdx-a",
+ "label": "patient-derived xenograft",
+ "short": "PDX",
+ "expansion": "patient-derived xenograft",
+ "firstUseSection": "abstract",
+ "expandedAtFirstUse": false,
+ "requiresDefinition": true,
+ "definition": "",
+ "jargonLevel": "high",
+ "observedForms": [
+ "PDX",
+ "patient derived xenograft"
+ ],
+ "preferredForm": "patient-derived xenograft",
+ "audience": "expert"
+ },
+ {
+ "id": "term-pdx-b",
+ "label": "pharmacodynamic index",
+ "short": "PDX",
+ "expansion": "pharmacodynamic index",
+ "firstUseSection": "results",
+ "expandedAtFirstUse": true,
+ "requiresDefinition": true,
+ "definition": "A composite drug-response index used in the exploratory analysis.",
+ "jargonLevel": "high",
+ "observedForms": [
+ "PDX",
+ "pharmacodynamic index"
+ ],
+ "preferredForm": "pharmacodynamic index"
+ },
+ {
+ "id": "term-erbb2",
+ "label": "ERBB2",
+ "firstUseSection": "title",
+ "expandedAtFirstUse": true,
+ "requiresDefinition": true,
+ "definition": "",
+ "jargonLevel": "high",
+ "expectedStyle": "HGNC gene symbol",
+ "observedStyle": "protein alias",
+ "observedForms": [
+ "HER2",
+ "ERBB2-positive",
+ "Her-2"
+ ],
+ "preferredForm": "ERBB2"
+ },
+ {
+ "id": "term-organoid",
+ "label": "organoid viability score",
+ "firstUseSection": "lay_summary",
+ "expandedAtFirstUse": true,
+ "requiresDefinition": true,
+ "definition": "A normalized assay readout for live tumor organoid cells after treatment.",
+ "jargonLevel": "high",
+ "audience": "lay",
+ "observedForms": [
+ "organoid viability score",
+ "OVS"
+ ],
+ "preferredForm": "organoid viability score"
+ }
+ ],
+ "citationRecommendations": [
+ {
+ "id": "cite-pdx-model",
+ "termId": "term-pdx-a",
+ "reason": "Recommended for first use of PDX model."
+ },
+ {
+ "id": "cite-unknown",
+ "termId": "term-nonexistent",
+ "reason": "Unbound recommendation from retrieval layer."
+ }
+ ],
+ "generatedSummaries": [
+ {
+ "id": "summary-lay",
+ "mode": "layperson",
+ "termIds": [
+ "term-pdx-a",
+ "term-organoid"
+ ]
+ }
+ ]
+ },
+ "evaluation": {
+ "summary": {
+ "manuscriptId": "oncology-organoid-draft-v1",
+ "decision": "hold_ai_research_packet",
+ "termsReviewed": 4,
+ "citationRecommendationsReviewed": 2,
+ "generatedSummariesReviewed": 1,
+ "findingCount": 12,
+ "highOrCriticalFindings": 6,
+ "auditDigest": "sha256:525040b780da7400"
+ },
+ "coverage": [
+ {
+ "id": "term-pdx-a",
+ "label": "patient-derived xenograft",
+ "short": "PDX",
+ "firstUseSection": "abstract",
+ "hasDefinition": false,
+ "hasLayDefinition": false,
+ "observedForms": [
+ "PDX",
+ "patient derived xenograft"
+ ]
+ },
+ {
+ "id": "term-pdx-b",
+ "label": "pharmacodynamic index",
+ "short": "PDX",
+ "firstUseSection": "results",
+ "hasDefinition": true,
+ "hasLayDefinition": false,
+ "observedForms": [
+ "PDX",
+ "pharmacodynamic index"
+ ]
+ },
+ {
+ "id": "term-erbb2",
+ "label": "ERBB2",
+ "short": null,
+ "firstUseSection": "title",
+ "hasDefinition": false,
+ "hasLayDefinition": false,
+ "observedForms": [
+ "HER2",
+ "ERBB2-positive",
+ "Her-2"
+ ]
+ },
+ {
+ "id": "term-organoid",
+ "label": "organoid viability score",
+ "short": null,
+ "firstUseSection": "lay_summary",
+ "hasDefinition": true,
+ "hasLayDefinition": false,
+ "observedForms": [
+ "organoid viability score",
+ "OVS"
+ ]
+ }
+ ],
+ "findings": [
+ {
+ "severity": "high",
+ "code": "ACRONYM_EXPANSION_CONFLICT",
+ "message": "PDX maps to 2 different expansions in the manuscript packet.",
+ "refs": [
+ "term-pdx-a",
+ "term-pdx-b"
+ ],
+ "action": "resolve_acronym_expansion_conflict"
+ },
+ {
+ "severity": "high",
+ "code": "ACRONYM_NOT_EXPANDED_AT_FIRST_USE",
+ "message": "PDX is not expanded at first use in abstract.",
+ "refs": [
+ "term-pdx-a"
+ ],
+ "action": "expand_acronym_at_first_use"
+ },
+ {
+ "severity": "high",
+ "code": "LAY_SUMMARY_JARGON_UNEXPLAINED",
+ "message": "Generated layperson summary summary-lay includes patient-derived xenograft without an accessible definition.",
+ "refs": [
+ "summary-lay",
+ "term-pdx-a"
+ ],
+ "action": "rewrite_lay_summary_with_definition"
+ },
+ {
+ "severity": "high",
+ "code": "LAY_SUMMARY_JARGON_UNEXPLAINED",
+ "message": "Generated layperson summary summary-lay includes organoid viability score without an accessible definition.",
+ "refs": [
+ "summary-lay",
+ "term-organoid"
+ ],
+ "action": "rewrite_lay_summary_with_definition"
+ },
+ {
+ "severity": "high",
+ "code": "TERM_DEFINITION_MISSING",
+ "message": "patient-derived xenograft requires a reviewer-facing definition before AI summaries or citation suggestions use it.",
+ "refs": [
+ "term-pdx-a"
+ ],
+ "action": "add_reviewer_facing_definition"
+ },
+ {
+ "severity": "high",
+ "code": "TERM_DEFINITION_MISSING",
+ "message": "ERBB2 requires a reviewer-facing definition before AI summaries or citation suggestions use it.",
+ "refs": [
+ "term-erbb2"
+ ],
+ "action": "add_reviewer_facing_definition"
+ },
+ {
+ "severity": "medium",
+ "code": "CITATION_BEFORE_TERM_DEFINITION",
+ "message": "Citation recommendation cite-pdx-model uses patient-derived xenograft before the term is defined.",
+ "refs": [
+ "cite-pdx-model",
+ "term-pdx-a"
+ ],
+ "action": "define_term_before_citation_insertion"
+ },
+ {
+ "severity": "medium",
+ "code": "CITATION_TERM_UNKNOWN",
+ "message": "Citation recommendation cite-unknown references an unknown terminology id.",
+ "refs": [
+ "cite-unknown"
+ ],
+ "action": "bind_citation_to_known_term"
+ },
+ {
+ "severity": "medium",
+ "code": "LAY_DEFINITION_MISSING",
+ "message": "organoid viability score appears in a lay-facing packet without a lay definition.",
+ "refs": [
+ "term-organoid"
+ ],
+ "action": "add_lay_definition_or_remove_from_lay_summary"
+ },
+ {
+ "severity": "medium",
+ "code": "NOMENCLATURE_STYLE_MISMATCH",
+ "message": "ERBB2 uses protein alias style where HGNC gene symbol style is expected.",
+ "refs": [
+ "term-erbb2"
+ ],
+ "action": "fix_domain_nomenclature_style"
+ },
+ {
+ "severity": "medium",
+ "code": "PREFERRED_TERM_FORM_ABSENT",
+ "message": "patient-derived xenograft has multiple observed forms but none match the preferred nomenclature.",
+ "refs": [
+ "term-pdx-a"
+ ],
+ "action": "normalize_term_to_preferred_form"
+ },
+ {
+ "severity": "medium",
+ "code": "PREFERRED_TERM_FORM_ABSENT",
+ "message": "ERBB2 has multiple observed forms but none match the preferred nomenclature.",
+ "refs": [
+ "term-erbb2"
+ ],
+ "action": "normalize_term_to_preferred_form"
+ }
+ ],
+ "actions": [
+ {
+ "id": "resolve_acronym_expansion_conflict",
+ "severity": "high",
+ "refs": [
+ "term-pdx-a",
+ "term-pdx-b"
+ ]
+ },
+ {
+ "id": "expand_acronym_at_first_use",
+ "severity": "high",
+ "refs": [
+ "term-pdx-a"
+ ]
+ },
+ {
+ "id": "rewrite_lay_summary_with_definition",
+ "severity": "high",
+ "refs": [
+ "summary-lay",
+ "term-pdx-a"
+ ]
+ },
+ {
+ "id": "add_reviewer_facing_definition",
+ "severity": "high",
+ "refs": [
+ "term-pdx-a"
+ ]
+ },
+ {
+ "id": "define_term_before_citation_insertion",
+ "severity": "medium",
+ "refs": [
+ "cite-pdx-model",
+ "term-pdx-a"
+ ]
+ },
+ {
+ "id": "bind_citation_to_known_term",
+ "severity": "medium",
+ "refs": [
+ "cite-unknown"
+ ]
+ },
+ {
+ "id": "add_lay_definition_or_remove_from_lay_summary",
+ "severity": "medium",
+ "refs": [
+ "term-organoid"
+ ]
+ },
+ {
+ "id": "fix_domain_nomenclature_style",
+ "severity": "medium",
+ "refs": [
+ "term-erbb2"
+ ]
+ },
+ {
+ "id": "normalize_term_to_preferred_form",
+ "severity": "medium",
+ "refs": [
+ "term-pdx-a"
+ ]
+ }
+ ]
+ }
+}
diff --git a/manuscript-terminology-definition-assistant/reports/summary.svg b/manuscript-terminology-definition-assistant/reports/summary.svg
new file mode 100644
index 00000000..c620d4a0
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/reports/summary.svg
@@ -0,0 +1,16 @@
+
+
diff --git a/manuscript-terminology-definition-assistant/reports/terminology-review-report.md b/manuscript-terminology-definition-assistant/reports/terminology-review-report.md
new file mode 100644
index 00000000..c53a6b10
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/reports/terminology-review-report.md
@@ -0,0 +1,32 @@
+# Manuscript Terminology Definition Review: oncology-organoid-draft-v1
+
+Decision: **hold_ai_research_packet**
+Audit digest: `sha256:525040b780da7400`
+
+## Findings
+
+| Severity | Code | Message | Action |
+| --- | --- | --- | --- |
+| high | `ACRONYM_EXPANSION_CONFLICT` | PDX maps to 2 different expansions in the manuscript packet. | `resolve_acronym_expansion_conflict` |
+| high | `ACRONYM_NOT_EXPANDED_AT_FIRST_USE` | PDX is not expanded at first use in abstract. | `expand_acronym_at_first_use` |
+| high | `LAY_SUMMARY_JARGON_UNEXPLAINED` | Generated layperson summary summary-lay includes patient-derived xenograft without an accessible definition. | `rewrite_lay_summary_with_definition` |
+| high | `LAY_SUMMARY_JARGON_UNEXPLAINED` | Generated layperson summary summary-lay includes organoid viability score without an accessible definition. | `rewrite_lay_summary_with_definition` |
+| high | `TERM_DEFINITION_MISSING` | patient-derived xenograft requires a reviewer-facing definition before AI summaries or citation suggestions use it. | `add_reviewer_facing_definition` |
+| high | `TERM_DEFINITION_MISSING` | ERBB2 requires a reviewer-facing definition before AI summaries or citation suggestions use it. | `add_reviewer_facing_definition` |
+| medium | `CITATION_BEFORE_TERM_DEFINITION` | Citation recommendation cite-pdx-model uses patient-derived xenograft before the term is defined. | `define_term_before_citation_insertion` |
+| medium | `CITATION_TERM_UNKNOWN` | Citation recommendation cite-unknown references an unknown terminology id. | `bind_citation_to_known_term` |
+| medium | `LAY_DEFINITION_MISSING` | organoid viability score appears in a lay-facing packet without a lay definition. | `add_lay_definition_or_remove_from_lay_summary` |
+| medium | `NOMENCLATURE_STYLE_MISMATCH` | ERBB2 uses protein alias style where HGNC gene symbol style is expected. | `fix_domain_nomenclature_style` |
+| medium | `PREFERRED_TERM_FORM_ABSENT` | patient-derived xenograft has multiple observed forms but none match the preferred nomenclature. | `normalize_term_to_preferred_form` |
+| medium | `PREFERRED_TERM_FORM_ABSENT` | ERBB2 has multiple observed forms but none match the preferred nomenclature. | `normalize_term_to_preferred_form` |
+
+## Term Coverage
+
+| Term | Short | Section | Definition | Lay definition | Observed forms |
+| --- | --- | --- | --- | --- | --- |
+| patient-derived xenograft | PDX | abstract | no | no | PDX, patient derived xenograft |
+| pharmacodynamic index | PDX | results | yes | no | PDX, pharmacodynamic index |
+| ERBB2 | | title | no | no | HER2, ERBB2-positive, Her-2 |
+| organoid viability score | | lay_summary | yes | no | organoid viability score, OVS |
+
+Synthetic data only. No uploaded manuscripts, private corpora, citation indexes, external AI APIs, credentials, or payment systems are used.
diff --git a/manuscript-terminology-definition-assistant/sample-data.js b/manuscript-terminology-definition-assistant/sample-data.js
new file mode 100644
index 00000000..1684b911
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/sample-data.js
@@ -0,0 +1,141 @@
+const cleanPacket = {
+ manuscriptId: "astrocyte-calcium-preprint-v3",
+ terms: [
+ {
+ id: "term-gcamp",
+ label: "genetically encoded calcium indicator",
+ short: "GECI",
+ expansion: "genetically encoded calcium indicator",
+ firstUseSection: "abstract",
+ expandedAtFirstUse: true,
+ requiresDefinition: true,
+ definition: "A fluorescent protein sensor used to report intracellular calcium dynamics.",
+ layDefinition: "A lab-made sensor that glows when cell calcium changes.",
+ jargonLevel: "medium",
+ observedForms: ["genetically encoded calcium indicator", "GECI"],
+ preferredForm: "genetically encoded calcium indicator"
+ },
+ {
+ id: "term-gfap",
+ label: "glial fibrillary acidic protein",
+ short: "GFAP",
+ expansion: "glial fibrillary acidic protein",
+ firstUseSection: "methods",
+ expandedAtFirstUse: true,
+ requiresDefinition: true,
+ definition: "An astrocyte marker used to identify glial cells in the imaging cohort.",
+ layDefinition: "A marker that helps identify support cells in the brain.",
+ jargonLevel: "medium",
+ observedForms: ["glial fibrillary acidic protein", "GFAP"],
+ preferredForm: "glial fibrillary acidic protein"
+ },
+ {
+ id: "term-stat-model",
+ label: "mixed-effects model",
+ firstUseSection: "results",
+ expandedAtFirstUse: true,
+ requiresDefinition: true,
+ definition: "A statistical model that accounts for repeated observations from the same animal.",
+ layDefinition: "A model that avoids counting repeated measurements as unrelated.",
+ jargonLevel: "medium",
+ observedForms: ["mixed-effects model"],
+ preferredForm: "mixed-effects model"
+ }
+ ],
+ citationRecommendations: [
+ {
+ id: "cite-geci-method",
+ termId: "term-gcamp",
+ reason: "Supports calcium indicator methodology."
+ }
+ ],
+ generatedSummaries: [
+ {
+ id: "summary-lay",
+ mode: "layperson",
+ termIds: ["term-gcamp", "term-stat-model"]
+ }
+ ]
+};
+
+const riskyPacket = {
+ manuscriptId: "oncology-organoid-draft-v1",
+ terms: [
+ {
+ id: "term-pdx-a",
+ label: "patient-derived xenograft",
+ short: "PDX",
+ expansion: "patient-derived xenograft",
+ firstUseSection: "abstract",
+ expandedAtFirstUse: false,
+ requiresDefinition: true,
+ definition: "",
+ jargonLevel: "high",
+ observedForms: ["PDX", "patient derived xenograft"],
+ preferredForm: "patient-derived xenograft",
+ audience: "expert"
+ },
+ {
+ id: "term-pdx-b",
+ label: "pharmacodynamic index",
+ short: "PDX",
+ expansion: "pharmacodynamic index",
+ firstUseSection: "results",
+ expandedAtFirstUse: true,
+ requiresDefinition: true,
+ definition: "A composite drug-response index used in the exploratory analysis.",
+ jargonLevel: "high",
+ observedForms: ["PDX", "pharmacodynamic index"],
+ preferredForm: "pharmacodynamic index"
+ },
+ {
+ id: "term-erbb2",
+ label: "ERBB2",
+ firstUseSection: "title",
+ expandedAtFirstUse: true,
+ requiresDefinition: true,
+ definition: "",
+ jargonLevel: "high",
+ expectedStyle: "HGNC gene symbol",
+ observedStyle: "protein alias",
+ observedForms: ["HER2", "ERBB2-positive", "Her-2"],
+ preferredForm: "ERBB2"
+ },
+ {
+ id: "term-organoid",
+ label: "organoid viability score",
+ firstUseSection: "lay_summary",
+ expandedAtFirstUse: true,
+ requiresDefinition: true,
+ definition: "A normalized assay readout for live tumor organoid cells after treatment.",
+ jargonLevel: "high",
+ audience: "lay",
+ observedForms: ["organoid viability score", "OVS"],
+ preferredForm: "organoid viability score"
+ }
+ ],
+ citationRecommendations: [
+ {
+ id: "cite-pdx-model",
+ termId: "term-pdx-a",
+ reason: "Recommended for first use of PDX model."
+ },
+ {
+ id: "cite-unknown",
+ termId: "term-nonexistent",
+ reason: "Unbound recommendation from retrieval layer."
+ }
+ ],
+ generatedSummaries: [
+ {
+ id: "summary-lay",
+ mode: "layperson",
+ termIds: ["term-pdx-a", "term-organoid"]
+ }
+ ]
+};
+
+module.exports = {
+ cleanPacket,
+ riskyPacket
+};
diff --git a/manuscript-terminology-definition-assistant/test.js b/manuscript-terminology-definition-assistant/test.js
new file mode 100644
index 00000000..948ca97e
--- /dev/null
+++ b/manuscript-terminology-definition-assistant/test.js
@@ -0,0 +1,42 @@
+const assert = require("node:assert/strict");
+const { evaluateTerminologyPacket, renderMarkdownReport, renderSvgSummary } = require("./index");
+const { cleanPacket, riskyPacket } = require("./sample-data");
+
+function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+function codes(evaluation) {
+ return new Set(evaluation.findings.map((finding) => finding.code));
+}
+
+const clean = evaluateTerminologyPacket(cleanPacket);
+assert.equal(clean.summary.decision, "release_ai_research_packet");
+assert.equal(clean.findings.length, 0);
+
+const risky = evaluateTerminologyPacket(riskyPacket);
+assert.equal(risky.summary.decision, "hold_ai_research_packet");
+assert.equal(codes(risky).has("ACRONYM_NOT_EXPANDED_AT_FIRST_USE"), true);
+assert.equal(codes(risky).has("TERM_DEFINITION_MISSING"), true);
+assert.equal(codes(risky).has("ACRONYM_EXPANSION_CONFLICT"), true);
+assert.equal(codes(risky).has("NOMENCLATURE_STYLE_MISMATCH"), true);
+assert.equal(codes(risky).has("CITATION_BEFORE_TERM_DEFINITION"), true);
+assert.equal(codes(risky).has("CITATION_TERM_UNKNOWN"), true);
+assert.equal(codes(risky).has("LAY_SUMMARY_JARGON_UNEXPLAINED"), true);
+
+const reviseOnly = clone(cleanPacket);
+reviseOnly.terms[1].observedForms = ["GFAP", "Gfap marker"];
+reviseOnly.terms[1].preferredForm = "glial fibrillary acidic protein";
+const revise = evaluateTerminologyPacket(reviseOnly);
+assert.equal(revise.summary.decision, "revise_terminology_packet");
+assert.equal(codes(revise).has("PREFERRED_TERM_FORM_ABSENT"), true);
+
+const markdown = renderMarkdownReport(riskyPacket, risky);
+assert.match(markdown, /Manuscript Terminology Definition Review/);
+assert.match(markdown, /hold_ai_research_packet/);
+
+const svg = renderSvgSummary(risky);
+assert.match(svg, /