diff --git a/research-materials-authentication-assistant/README.md b/research-materials-authentication-assistant/README.md
new file mode 100644
index 00000000..7d54a83a
--- /dev/null
+++ b/research-materials-authentication-assistant/README.md
@@ -0,0 +1,54 @@
+# Research Materials Authentication Assistant
+
+This module adds a focused research-materials authentication guard for the AI-Powered Research Assistant Suite in issue `#16`.
+
+AI-assisted review can miss mundane but decisive reproducibility problems: an antibody without a traceable identifier, a stale validation panel, a contaminated cell line, a species mismatch, or a passage range that cannot be true. This assistant inspects synthetic manuscript review packets and returns release, review, or hold decisions with evidence-linked remediation.
+
+## Scope
+
+- Checks RRID-style identifiers for antibodies, cell lines, organism strains, primary cells, and reagents.
+- Flags missing lot or batch metadata for lot-sensitive materials.
+- Checks validation evidence freshness against a deterministic review date.
+- Holds contaminated cell-derived materials.
+- Checks reported passage ranges and reported passage numbers.
+- Compares material species and claimed organism against linked assays.
+- Holds antibody target mismatches when the antibody target does not match the measured assay target.
+- Produces JSON, Markdown, SVG, and MP4 demo artifacts.
+- Uses synthetic data only.
+- Uses no external AI APIs, credentials, private manuscripts, payment systems, or network calls.
+
+## Requirement Map
+
+| Issue #16 capability | Implementation |
+| --- | --- |
+| Auto peer review reports | Emits structured findings, severity, decision, confidence, and remediation. |
+| Methodological and reproducibility red flags | Detects identifier gaps, stale validation, contamination, passage errors, organism mismatch, and antibody target mismatch. |
+| Adaptive templates per domain | Supports packet-level `domain` and domain-specific reviewer notes. |
+| Claims vs evidence alignment | Compares material identity and validation evidence against linked assays and manuscript claims. |
+| Reproducible deterministic output | Demo and tests use fixed synthetic packets and dependency-free scoring. |
+
+## Run
+
+```bash
+npm test
+npm run demo
+npm run video
+npm run check
+```
+
+Demo artifacts are written to `reports/`:
+
+- `summary.json`
+- `reviewer-packet.md`
+- `summary.svg`
+- `demo.mp4`
+
+## Decisions
+
+- `RELEASE`: materials are traceable, authenticated, fresh, and aligned with the linked assays.
+- `REVIEW`: a reviewer should inspect missing identifiers, stale validation, species mismatch, or incomplete material provenance.
+- `HOLD`: release is blocked by critical reproducibility problems such as contamination, impossible passage metadata, or antibody target mismatch.
+
+## AI Assistance Disclosure
+
+Implemented with Codex assistance and locally verified with deterministic tests and generated artifacts.
diff --git a/research-materials-authentication-assistant/data/sample_material_packets.json b/research-materials-authentication-assistant/data/sample_material_packets.json
new file mode 100644
index 00000000..0c442fa4
--- /dev/null
+++ b/research-materials-authentication-assistant/data/sample_material_packets.json
@@ -0,0 +1,170 @@
+[
+ {
+ "id": "clean-organoid-panel",
+ "title": "Authenticated organoid cytokine response panel",
+ "domain": "translational immunology",
+ "reviewDate": "2026-08-15",
+ "materials": [
+ {
+ "name": "HBEC-Alpha authenticated airway cell line",
+ "type": "cell_line",
+ "role": "model system",
+ "rrid": "RRID:CVCL_SYN1",
+ "lot": "BANK-2026-07-A",
+ "source": "institutional biobank",
+ "species": "human",
+ "claimedOrganism": "human",
+ "validationEvidence": ["STR profile match", "certificate of analysis", "mycoplasma PCR negative"],
+ "validationDate": "2026-07-10",
+ "contaminationStatus": "negative",
+ "passageRange": { "min": 5, "max": 18 },
+ "reportedPassage": 9
+ },
+ {
+ "name": "Anti-IL6 clone S6",
+ "type": "antibody",
+ "role": "capture antibody",
+ "rrid": "RRID:AB_9000001",
+ "lot": "LOT-IL6-2606",
+ "source": "validated synthetic catalog",
+ "species": "human",
+ "target": "IL6",
+ "assayTargets": ["IL6"],
+ "validationEvidence": ["western blot specificity", "orthogonal ELISA spike recovery"],
+ "validationDate": "2026-06-14",
+ "contaminationStatus": "not_detected"
+ }
+ ],
+ "assays": [
+ {
+ "name": "IL6 secretion ELISA",
+ "materialName": "Anti-IL6 clone S6",
+ "organism": "human",
+ "target": "IL6",
+ "readout": "secreted cytokine concentration"
+ }
+ ],
+ "manuscriptClaims": [
+ "All primary materials were identity checked before cytokine-response analysis."
+ ]
+ },
+ {
+ "id": "review-stale-rrid-organism",
+ "title": "Stale antibody validation with organism mismatch",
+ "domain": "cell signaling",
+ "reviewDate": "2026-08-15",
+ "materials": [
+ {
+ "name": "Anti-pERK screening antibody",
+ "type": "antibody",
+ "role": "western blot antibody",
+ "rrid": "",
+ "lot": "WB-ERK-2019",
+ "source": "legacy freezer box",
+ "species": "mouse",
+ "claimedOrganism": "human",
+ "target": "pERK",
+ "assayTargets": ["pERK"],
+ "validationEvidence": ["vendor blot from 2019"],
+ "validationDate": "2019-03-04",
+ "contaminationStatus": "not_detected"
+ }
+ ],
+ "assays": [
+ {
+ "name": "Human pERK western blot",
+ "materialName": "Anti-pERK screening antibody",
+ "organism": "human",
+ "target": "pERK",
+ "readout": "phosphorylated ERK band intensity"
+ }
+ ],
+ "manuscriptClaims": [
+ "Human samples were assayed with pathway-specific antibody material."
+ ]
+ },
+ {
+ "id": "hold-contaminated-line",
+ "title": "Contaminated cell line in proliferation assay",
+ "domain": "cancer biology",
+ "reviewDate": "2026-08-15",
+ "materials": [
+ {
+ "name": "MCB-Tumor-2 working stock",
+ "type": "cell_line",
+ "role": "tumor model",
+ "rrid": "RRID:CVCL_SYN2",
+ "lot": "CELL-2026-05-B",
+ "source": "working stock",
+ "species": "human",
+ "claimedOrganism": "human",
+ "validationEvidence": ["STR profile match"],
+ "validationDate": "2026-05-12",
+ "contaminationStatus": "mycoplasma_positive",
+ "passageRange": { "min": 3, "max": 14 },
+ "reportedPassage": 11
+ }
+ ],
+ "assays": [
+ {
+ "name": "Proliferation growth curve",
+ "materialName": "MCB-Tumor-2 working stock",
+ "organism": "human",
+ "target": "cell growth",
+ "readout": "confluence over time"
+ }
+ ],
+ "manuscriptClaims": [
+ "The tumor cell model supports the proliferation finding."
+ ]
+ },
+ {
+ "id": "hold-antibody-passage",
+ "title": "Antibody target mismatch and impossible passage metadata",
+ "domain": "neurobiology",
+ "reviewDate": "2026-08-15",
+ "materials": [
+ {
+ "name": "NeuroStem-X passage record",
+ "type": "primary_cell",
+ "role": "differentiation model",
+ "rrid": "RRID:CVCL_SYN3",
+ "lot": "NSX-2601",
+ "source": "synthetic stem-cell bank",
+ "species": "human",
+ "claimedOrganism": "human",
+ "validationEvidence": ["identity PCR panel"],
+ "validationDate": "2026-01-15",
+ "contaminationStatus": "negative",
+ "passageRange": { "min": 22, "max": 8 },
+ "reportedPassage": 31
+ },
+ {
+ "name": "Anti-GFAP clone G4",
+ "type": "antibody",
+ "role": "immunostaining antibody",
+ "rrid": "RRID:AB_9000002",
+ "lot": "LOT-GFAP-2601",
+ "source": "validated synthetic catalog",
+ "species": "human",
+ "target": "GFAP",
+ "assayTargets": ["GFAP"],
+ "validationEvidence": ["knockdown specificity panel"],
+ "validationDate": "2026-01-20",
+ "contaminationStatus": "not_detected"
+ }
+ ],
+ "assays": [
+ {
+ "name": "MAP2 neuron staining",
+ "materialName": "Anti-GFAP clone G4",
+ "organism": "human",
+ "target": "MAP2",
+ "readout": "fluorescent neurite area"
+ }
+ ],
+ "manuscriptClaims": [
+ "Neuron differentiation was quantified with validated immunostaining material."
+ ]
+ }
+]
diff --git a/research-materials-authentication-assistant/package.json b/research-materials-authentication-assistant/package.json
new file mode 100644
index 00000000..2ae00e43
--- /dev/null
+++ b/research-materials-authentication-assistant/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "research-materials-authentication-assistant",
+ "version": "1.0.0",
+ "description": "Dependency-free research materials authentication guard for AI research assistant review packets.",
+ "main": "src/index.js",
+ "scripts": {
+ "test": "node --test",
+ "demo": "node scripts/demo.js",
+ "video": "node scripts/render-demo-video.js",
+ "check": "node --check src/index.js && node --check scripts/demo.js && node --check scripts/render-demo-video.js"
+ },
+ "license": "MIT"
+}
diff --git a/research-materials-authentication-assistant/reports/demo.mp4 b/research-materials-authentication-assistant/reports/demo.mp4
new file mode 100644
index 00000000..11770451
Binary files /dev/null and b/research-materials-authentication-assistant/reports/demo.mp4 differ
diff --git a/research-materials-authentication-assistant/reports/reviewer-packet.md b/research-materials-authentication-assistant/reports/reviewer-packet.md
new file mode 100644
index 00000000..d1b9efd9
--- /dev/null
+++ b/research-materials-authentication-assistant/reports/reviewer-packet.md
@@ -0,0 +1,56 @@
+# Research Materials Authentication Review
+
+Generated: 2026-08-15T20:45:50.858Z
+
+## Decision Counts
+
+- RELEASE: 1
+- REVIEW: 1
+- HOLD: 2
+
+## Packet Reviews
+
+### Authenticated organoid cytokine response panel
+Decision: RELEASE
+Confidence: 0.96
+Materials checked: 2
+Assays checked: 1
+Reviewer note: translational immunology: materials are traceable, authenticated, and aligned with linked assays.
+
+### Stale antibody validation with organism mismatch
+Decision: REVIEW
+Confidence: 0.12
+Materials checked: 1
+Assays checked: 1
+Reviewer note: cell signaling: REVIEW before release because a tracked research material is missing a valid rrid-style identifier.
+- MAJOR MISSING_OR_INVALID_RRID: A tracked research material is missing a valid RRID-style identifier.
+ Remediation: Add the RRID or equivalent registry identifier used for the exact material instance.
+- MAJOR STALE_VALIDATION_DATE: The latest validation evidence is older than the configured freshness window.
+ Remediation: Refresh material authentication or justify why the prior validation still applies.
+- MAJOR MATERIAL_CLAIM_ORGANISM_MISMATCH: The material species does not match the organism claimed in the manuscript packet.
+ Remediation: Correct the claimed organism or provide a cross-species rationale and validation evidence.
+- MAJOR ASSAY_ORGANISM_MISMATCH: The material species does not match the organism declared for the linked assay.
+ Remediation: Route the packet for reviewer confirmation or correct the assay-material mapping.
+
+### Contaminated cell line in proliferation assay
+Decision: HOLD
+Confidence: 0.61
+Materials checked: 1
+Assays checked: 1
+Reviewer note: cancer biology: HOLD before release because a cell-derived material is marked contaminated or contamination-positive.
+- CRITICAL CONTAMINATED_CELL_MATERIAL: A cell-derived material is marked contaminated or contamination-positive.
+ Remediation: Hold release until the assay is repeated with a contamination-negative authenticated material.
+
+### Antibody target mismatch and impossible passage metadata
+Decision: HOLD
+Confidence: 0.1
+Materials checked: 2
+Assays checked: 1
+Reviewer note: neurobiology: HOLD before release because the packet reports an impossible or biologically implausible passage range.
+- CRITICAL IMPOSSIBLE_PASSAGE_RANGE: The packet reports an impossible or biologically implausible passage range.
+ Remediation: Correct the passage metadata and provide source authentication records before the assistant releases the review.
+- CRITICAL ANTIBODY_TARGET_MISMATCH: The antibody target does not match the target measured by the linked assay.
+ Remediation: Hold release until the antibody, assay target, or interpretation is corrected and revalidated.
+- MAJOR ASSAY_TARGET_NOT_VALIDATED_FOR_MATERIAL: The assay target is outside the target set validated for this material.
+ Remediation: Add target-specific validation evidence or remap the assay to an authenticated material.
+
diff --git a/research-materials-authentication-assistant/reports/summary.json b/research-materials-authentication-assistant/reports/summary.json
new file mode 100644
index 00000000..8e708bd3
--- /dev/null
+++ b/research-materials-authentication-assistant/reports/summary.json
@@ -0,0 +1,156 @@
+{
+ "generatedAt": "2026-08-15T20:45:50.858Z",
+ "packetCount": 4,
+ "decisionCounts": {
+ "RELEASE": 1,
+ "REVIEW": 1,
+ "HOLD": 2
+ },
+ "reviews": [
+ {
+ "id": "clean-organoid-panel",
+ "title": "Authenticated organoid cytokine response panel",
+ "domain": "translational immunology",
+ "decision": "RELEASE",
+ "confidence": 0.96,
+ "materialCount": 2,
+ "assayCount": 1,
+ "findingCount": 0,
+ "findings": [],
+ "reviewerNote": "translational immunology: materials are traceable, authenticated, and aligned with linked assays."
+ },
+ {
+ "id": "review-stale-rrid-organism",
+ "title": "Stale antibody validation with organism mismatch",
+ "domain": "cell signaling",
+ "decision": "REVIEW",
+ "confidence": 0.12,
+ "materialCount": 1,
+ "assayCount": 1,
+ "findingCount": 4,
+ "findings": [
+ {
+ "code": "MISSING_OR_INVALID_RRID",
+ "severity": "major",
+ "message": "A tracked research material is missing a valid RRID-style identifier.",
+ "evidence": {
+ "material": "Anti-pERK screening antibody",
+ "type": "antibody",
+ "rrid": null
+ },
+ "remediation": "Add the RRID or equivalent registry identifier used for the exact material instance."
+ },
+ {
+ "code": "STALE_VALIDATION_DATE",
+ "severity": "major",
+ "message": "The latest validation evidence is older than the configured freshness window.",
+ "evidence": {
+ "material": "Anti-pERK screening antibody",
+ "validationDate": "2019-03-04",
+ "staleAfterDays": 730
+ },
+ "remediation": "Refresh material authentication or justify why the prior validation still applies."
+ },
+ {
+ "code": "MATERIAL_CLAIM_ORGANISM_MISMATCH",
+ "severity": "major",
+ "message": "The material species does not match the organism claimed in the manuscript packet.",
+ "evidence": {
+ "material": "Anti-pERK screening antibody",
+ "species": "mouse",
+ "claimedOrganism": "human"
+ },
+ "remediation": "Correct the claimed organism or provide a cross-species rationale and validation evidence."
+ },
+ {
+ "code": "ASSAY_ORGANISM_MISMATCH",
+ "severity": "major",
+ "message": "The material species does not match the organism declared for the linked assay.",
+ "evidence": {
+ "material": "Anti-pERK screening antibody",
+ "materialSpecies": "mouse",
+ "assay": "Human pERK western blot",
+ "assayOrganism": "human"
+ },
+ "remediation": "Route the packet for reviewer confirmation or correct the assay-material mapping."
+ }
+ ],
+ "reviewerNote": "cell signaling: REVIEW before release because a tracked research material is missing a valid rrid-style identifier."
+ },
+ {
+ "id": "hold-contaminated-line",
+ "title": "Contaminated cell line in proliferation assay",
+ "domain": "cancer biology",
+ "decision": "HOLD",
+ "confidence": 0.61,
+ "materialCount": 1,
+ "assayCount": 1,
+ "findingCount": 1,
+ "findings": [
+ {
+ "code": "CONTAMINATED_CELL_MATERIAL",
+ "severity": "critical",
+ "message": "A cell-derived material is marked contaminated or contamination-positive.",
+ "evidence": {
+ "material": "MCB-Tumor-2 working stock",
+ "contaminationStatus": "mycoplasma_positive"
+ },
+ "remediation": "Hold release until the assay is repeated with a contamination-negative authenticated material."
+ }
+ ],
+ "reviewerNote": "cancer biology: HOLD before release because a cell-derived material is marked contaminated or contamination-positive."
+ },
+ {
+ "id": "hold-antibody-passage",
+ "title": "Antibody target mismatch and impossible passage metadata",
+ "domain": "neurobiology",
+ "decision": "HOLD",
+ "confidence": 0.1,
+ "materialCount": 2,
+ "assayCount": 1,
+ "findingCount": 3,
+ "findings": [
+ {
+ "code": "IMPOSSIBLE_PASSAGE_RANGE",
+ "severity": "critical",
+ "message": "The packet reports an impossible or biologically implausible passage range.",
+ "evidence": {
+ "material": "NeuroStem-X passage record",
+ "passageRange": {
+ "min": 22,
+ "max": 8
+ }
+ },
+ "remediation": "Correct the passage metadata and provide source authentication records before the assistant releases the review."
+ },
+ {
+ "code": "ANTIBODY_TARGET_MISMATCH",
+ "severity": "critical",
+ "message": "The antibody target does not match the target measured by the linked assay.",
+ "evidence": {
+ "material": "Anti-GFAP clone G4",
+ "antibodyTarget": "GFAP",
+ "assay": "MAP2 neuron staining",
+ "assayTarget": "MAP2"
+ },
+ "remediation": "Hold release until the antibody, assay target, or interpretation is corrected and revalidated."
+ },
+ {
+ "code": "ASSAY_TARGET_NOT_VALIDATED_FOR_MATERIAL",
+ "severity": "major",
+ "message": "The assay target is outside the target set validated for this material.",
+ "evidence": {
+ "material": "Anti-GFAP clone G4",
+ "assay": "MAP2 neuron staining",
+ "assayTarget": "MAP2",
+ "validatedTargets": [
+ "GFAP"
+ ]
+ },
+ "remediation": "Add target-specific validation evidence or remap the assay to an authenticated material."
+ }
+ ],
+ "reviewerNote": "neurobiology: HOLD before release because the packet reports an impossible or biologically implausible passage range."
+ }
+ ]
+}
diff --git a/research-materials-authentication-assistant/reports/summary.svg b/research-materials-authentication-assistant/reports/summary.svg
new file mode 100644
index 00000000..512a4c90
--- /dev/null
+++ b/research-materials-authentication-assistant/reports/summary.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/research-materials-authentication-assistant/scripts/demo.js b/research-materials-authentication-assistant/scripts/demo.js
new file mode 100644
index 00000000..934fcb68
--- /dev/null
+++ b/research-materials-authentication-assistant/scripts/demo.js
@@ -0,0 +1,76 @@
+"use strict";
+
+const fs = require("node:fs");
+const path = require("node:path");
+const { analyzePackets } = require("../src");
+
+const root = path.resolve(__dirname, "..");
+const packets = JSON.parse(fs.readFileSync(path.join(root, "data", "sample_material_packets.json"), "utf8"));
+const report = analyzePackets(packets);
+const outDir = path.join(root, "reports");
+
+fs.mkdirSync(outDir, { recursive: true });
+fs.writeFileSync(path.join(outDir, "summary.json"), `${JSON.stringify(report, null, 2)}\n`);
+fs.writeFileSync(path.join(outDir, "reviewer-packet.md"), renderMarkdown(report));
+fs.writeFileSync(path.join(outDir, "summary.svg"), renderSvg(report));
+
+console.log(JSON.stringify({
+ packetCount: report.packetCount,
+ decisionCounts: report.decisionCounts,
+ reports: [
+ "reports/summary.json",
+ "reports/reviewer-packet.md",
+ "reports/summary.svg"
+ ]
+}, null, 2));
+
+function renderMarkdown(report) {
+ const lines = [
+ "# Research Materials Authentication Review",
+ "",
+ `Generated: ${report.generatedAt}`,
+ "",
+ "## Decision Counts",
+ "",
+ `- RELEASE: ${report.decisionCounts.RELEASE}`,
+ `- REVIEW: ${report.decisionCounts.REVIEW}`,
+ `- HOLD: ${report.decisionCounts.HOLD}`,
+ "",
+ "## Packet Reviews",
+ ""
+ ];
+
+ for (const review of report.reviews) {
+ lines.push(`### ${review.title}`);
+ lines.push(`Decision: ${review.decision}`);
+ lines.push(`Confidence: ${review.confidence}`);
+ lines.push(`Materials checked: ${review.materialCount}`);
+ lines.push(`Assays checked: ${review.assayCount}`);
+ lines.push(`Reviewer note: ${review.reviewerNote}`);
+ for (const item of review.findings) {
+ lines.push(`- ${item.severity.toUpperCase()} ${item.code}: ${item.message}`);
+ lines.push(` Remediation: ${item.remediation}`);
+ }
+ lines.push("");
+ }
+
+ return `${lines.join("\n")}\n`;
+}
+
+function renderSvg(report) {
+ return ``;
+}
+
+function bar(label, count, x, y, color) {
+ const width = 96 + count * 116;
+ return `${label}: ${count}
+
+ `;
+}
diff --git a/research-materials-authentication-assistant/scripts/render-demo-video.js b/research-materials-authentication-assistant/scripts/render-demo-video.js
new file mode 100644
index 00000000..106fe046
--- /dev/null
+++ b/research-materials-authentication-assistant/scripts/render-demo-video.js
@@ -0,0 +1,60 @@
+"use strict";
+
+const fs = require("node:fs");
+const path = require("node:path");
+const { spawnSync } = require("node:child_process");
+
+const root = path.resolve(__dirname, "..");
+const ffmpeg = path.resolve(
+ root,
+ "..",
+ "..",
+ "tool_downloads",
+ "video_tools",
+ "node_modules",
+ "ffmpeg-static",
+ "ffmpeg.exe"
+);
+const outDir = path.join(root, "reports");
+const out = path.join(outDir, "demo.mp4");
+const font = "C\\:/Windows/Fonts/arial.ttf";
+
+fs.mkdirSync(outDir, { recursive: true });
+
+const draw = [
+ `drawtext=fontfile=${font}:text='Research Materials Authentication':x=64:y=56:fontsize=44:fontcolor=black`,
+ `drawtext=fontfile=${font}:text='AI peer-review gate for biological materials identity':x=64:y=120:fontsize=25:fontcolor=0x344052`,
+ `drawtext=fontfile=${font}:text='Synthetic packets analyzed 4':x=64:y=210:fontsize=34:fontcolor=black`,
+ `drawtext=fontfile=${font}:text='RELEASE 1 REVIEW 1 HOLD 2':x=64:y=268:fontsize=38:fontcolor=0x245fd6`,
+ `drawtext=fontfile=${font}:text='Checks RRIDs lots validation dates contamination passage ranges':x=64:y=358:fontsize=26:fontcolor=0x344052`,
+ `drawtext=fontfile=${font}:text='Flags organism mismatch antibody target mismatch stale evidence':x=64:y=422:fontsize=26:fontcolor=0x344052`,
+ `drawtext=fontfile=${font}:text='No external APIs credentials private manuscripts or live data':x=64:y=486:fontsize=26:fontcolor=0x344052`
+].join(",");
+
+if (!fs.existsSync(ffmpeg)) {
+ throw new Error(`ffmpeg binary not found at ${ffmpeg}`);
+}
+
+const result = spawnSync(ffmpeg, [
+ "-y",
+ "-f",
+ "lavfi",
+ "-i",
+ "color=c=0xf7f9fb:s=1280x720:d=8:r=30",
+ "-vf",
+ draw,
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-movflags",
+ "+faststart",
+ out
+], { encoding: "utf8" });
+
+if (result.status !== 0) {
+ process.stderr.write(result.stderr || result.stdout);
+ process.exit(result.status || 1);
+}
+
+console.log(JSON.stringify({ out, bytes: fs.statSync(out).size }, null, 2));
diff --git a/research-materials-authentication-assistant/src/index.js b/research-materials-authentication-assistant/src/index.js
new file mode 100644
index 00000000..959cc993
--- /dev/null
+++ b/research-materials-authentication-assistant/src/index.js
@@ -0,0 +1,333 @@
+"use strict";
+
+const TRACKED_MATERIAL_TYPES = new Set([
+ "antibody",
+ "cell_line",
+ "organism_strain",
+ "primary_cell",
+ "reagent"
+]);
+
+const STALE_VALIDATION_DAYS = 730;
+const CLEAN_CONTAMINATION_VALUES = new Set(["negative", "clear", "not_detected", "none"]);
+const CONTAMINATION_ALERT_VALUES = new Set(["positive", "detected", "contaminated", "mycoplasma_positive"]);
+
+function finding(code, severity, message, evidence, remediation) {
+ return { code, severity, message, evidence, remediation };
+}
+
+function normalizeText(value) {
+ return String(value || "").trim().toLowerCase();
+}
+
+function normalizeMaterial(material) {
+ return {
+ name: material.name || material.id || "unnamed material",
+ type: normalizeText(material.type || "reagent"),
+ role: material.role || "unspecified",
+ rrid: material.rrid || "",
+ lot: material.lot || "",
+ source: material.source || "",
+ species: material.species || material.organism || "",
+ claimedOrganism: material.claimedOrganism || "",
+ target: material.target || "",
+ assayTargets: Array.isArray(material.assayTargets) ? material.assayTargets : [],
+ validationEvidence: Array.isArray(material.validationEvidence) ? material.validationEvidence : [],
+ validationDate: material.validationDate || "",
+ contaminationStatus: material.contaminationStatus || "",
+ passageRange: material.passageRange || null,
+ reportedPassage: material.reportedPassage,
+ metadata: material.metadata || {}
+ };
+}
+
+function normalizeAssay(assay) {
+ return {
+ name: assay.name || "unnamed assay",
+ materialName: assay.materialName || assay.material || "",
+ organism: assay.organism || assay.species || "",
+ target: assay.target || "",
+ readout: assay.readout || ""
+ };
+}
+
+function normalizePacket(packet) {
+ return {
+ id: packet.id,
+ title: packet.title || "Untitled research packet",
+ domain: packet.domain || "life sciences",
+ reviewDate: packet.reviewDate || "2026-08-15",
+ materials: Array.isArray(packet.materials) ? packet.materials.map(normalizeMaterial) : [],
+ assays: Array.isArray(packet.assays) ? packet.assays.map(normalizeAssay) : [],
+ manuscriptClaims: Array.isArray(packet.manuscriptClaims) ? packet.manuscriptClaims : []
+ };
+}
+
+function validRrid(value) {
+ return /^RRID:[A-Za-z0-9_:-]+$/.test(String(value || "").trim());
+}
+
+function parseDate(value) {
+ const time = Date.parse(value);
+ return Number.isFinite(time) ? new Date(time) : null;
+}
+
+function daysBetween(start, end) {
+ return Math.floor((end.getTime() - start.getTime()) / 86400000);
+}
+
+function sameConcept(left, right) {
+ if (!left || !right) return true;
+ return normalizeText(left) === normalizeText(right);
+}
+
+function materialMatchesAssay(material, assay) {
+ return normalizeText(material.name) === normalizeText(assay.materialName);
+}
+
+function assaysForMaterial(material, assays) {
+ return assays.filter((assay) => materialMatchesAssay(material, assay));
+}
+
+function analyzePassageRange(material) {
+ const findings = [];
+ const range = material.passageRange;
+ if (!range) return findings;
+
+ const min = Number(range.min);
+ const max = Number(range.max);
+ const reported = material.reportedPassage === undefined ? null : Number(material.reportedPassage);
+
+ if (!Number.isFinite(min) || !Number.isFinite(max) || min < 0 || max < min || max > 250) {
+ findings.push(finding(
+ "IMPOSSIBLE_PASSAGE_RANGE",
+ "critical",
+ "The packet reports an impossible or biologically implausible passage range.",
+ { material: material.name, passageRange: range },
+ "Correct the passage metadata and provide source authentication records before the assistant releases the review."
+ ));
+ return findings;
+ }
+
+ if (reported !== null && (!Number.isFinite(reported) || reported < min || reported > max)) {
+ findings.push(finding(
+ "PASSAGE_OUTSIDE_VALIDATED_RANGE",
+ "major",
+ "The reported passage is outside the validated passage range for this material.",
+ { material: material.name, passageRange: range, reportedPassage: material.reportedPassage },
+ "Provide validation evidence for the reported passage or repeat the assay using a validated passage."
+ ));
+ }
+
+ return findings;
+}
+
+function analyzeMaterial(material, assays, reviewDate) {
+ const findings = [];
+ const linkedAssays = assaysForMaterial(material, assays);
+
+ if (TRACKED_MATERIAL_TYPES.has(material.type) && !validRrid(material.rrid)) {
+ findings.push(finding(
+ "MISSING_OR_INVALID_RRID",
+ material.type === "reagent" ? "minor" : "major",
+ "A tracked research material is missing a valid RRID-style identifier.",
+ { material: material.name, type: material.type, rrid: material.rrid || null },
+ "Add the RRID or equivalent registry identifier used for the exact material instance."
+ ));
+ }
+
+ if ((material.type === "antibody" || material.type === "reagent") && !material.lot) {
+ findings.push(finding(
+ "MISSING_LOT_IDENTIFIER",
+ "minor",
+ "A lot-sensitive material is missing lot or batch information.",
+ { material: material.name, type: material.type },
+ "Add lot, batch, clone, or catalog metadata so reviewers can trace the exact material."
+ ));
+ }
+
+ if (TRACKED_MATERIAL_TYPES.has(material.type) && material.validationEvidence.length === 0) {
+ findings.push(finding(
+ "MISSING_VALIDATION_EVIDENCE",
+ "major",
+ "The packet does not include validation evidence for a tracked material.",
+ { material: material.name, type: material.type },
+ "Attach identity, specificity, genotype, certificate-of-analysis, or orthogonal validation evidence."
+ ));
+ }
+
+ const review = parseDate(reviewDate);
+ const validation = parseDate(material.validationDate);
+ if (!validation) {
+ findings.push(finding(
+ "MISSING_VALIDATION_DATE",
+ "major",
+ "The packet does not provide a parseable validation date.",
+ { material: material.name, validationDate: material.validationDate || null },
+ "Provide the date of the latest identity or specificity validation."
+ ));
+ } else if (review && daysBetween(validation, review) > STALE_VALIDATION_DAYS) {
+ findings.push(finding(
+ "STALE_VALIDATION_DATE",
+ "major",
+ "The latest validation evidence is older than the configured freshness window.",
+ { material: material.name, validationDate: material.validationDate, staleAfterDays: STALE_VALIDATION_DAYS },
+ "Refresh material authentication or justify why the prior validation still applies."
+ ));
+ }
+
+ const contamination = normalizeText(material.contaminationStatus);
+ if (material.type === "cell_line" || material.type === "primary_cell") {
+ if (CONTAMINATION_ALERT_VALUES.has(contamination)) {
+ findings.push(finding(
+ "CONTAMINATED_CELL_MATERIAL",
+ "critical",
+ "A cell-derived material is marked contaminated or contamination-positive.",
+ { material: material.name, contaminationStatus: material.contaminationStatus },
+ "Hold release until the assay is repeated with a contamination-negative authenticated material."
+ ));
+ } else if (!CLEAN_CONTAMINATION_VALUES.has(contamination)) {
+ findings.push(finding(
+ "CONTAMINATION_STATUS_UNVERIFIED",
+ "major",
+ "A cell-derived material lacks a clear contamination-negative status.",
+ { material: material.name, contaminationStatus: material.contaminationStatus || null },
+ "Add recent mycoplasma and contamination testing before releasing the review."
+ ));
+ }
+ }
+
+ if (material.species && material.claimedOrganism && !sameConcept(material.species, material.claimedOrganism)) {
+ findings.push(finding(
+ "MATERIAL_CLAIM_ORGANISM_MISMATCH",
+ "major",
+ "The material species does not match the organism claimed in the manuscript packet.",
+ { material: material.name, species: material.species, claimedOrganism: material.claimedOrganism },
+ "Correct the claimed organism or provide a cross-species rationale and validation evidence."
+ ));
+ }
+
+ for (const assay of linkedAssays) {
+ if (material.species && assay.organism && !sameConcept(material.species, assay.organism)) {
+ findings.push(finding(
+ "ASSAY_ORGANISM_MISMATCH",
+ "major",
+ "The material species does not match the organism declared for the linked assay.",
+ { material: material.name, materialSpecies: material.species, assay: assay.name, assayOrganism: assay.organism },
+ "Route the packet for reviewer confirmation or correct the assay-material mapping."
+ ));
+ }
+
+ if (material.type === "antibody" && material.target && assay.target && !sameConcept(material.target, assay.target)) {
+ findings.push(finding(
+ "ANTIBODY_TARGET_MISMATCH",
+ "critical",
+ "The antibody target does not match the target measured by the linked assay.",
+ { material: material.name, antibodyTarget: material.target, assay: assay.name, assayTarget: assay.target },
+ "Hold release until the antibody, assay target, or interpretation is corrected and revalidated."
+ ));
+ }
+
+ if (material.assayTargets.length > 0 && assay.target) {
+ const allowed = material.assayTargets.map(normalizeText);
+ if (!allowed.includes(normalizeText(assay.target))) {
+ findings.push(finding(
+ "ASSAY_TARGET_NOT_VALIDATED_FOR_MATERIAL",
+ "major",
+ "The assay target is outside the target set validated for this material.",
+ { material: material.name, assay: assay.name, assayTarget: assay.target, validatedTargets: material.assayTargets },
+ "Add target-specific validation evidence or remap the assay to an authenticated material."
+ ));
+ }
+ }
+ }
+
+ findings.push(...analyzePassageRange(material));
+ return findings;
+}
+
+function severityWeight(severity) {
+ if (severity === "critical") return 5;
+ if (severity === "major") return 3;
+ return 1;
+}
+
+function decisionFromFindings(findings) {
+ if (findings.some((item) => item.severity === "critical")) return "HOLD";
+ if (findings.length > 0) return "REVIEW";
+ return "RELEASE";
+}
+
+function confidenceFromFindings(findings) {
+ const penalty = findings.reduce((total, item) => total + severityWeight(item.severity), 0);
+ return Math.max(0.1, Number((0.96 - penalty * 0.07).toFixed(2)));
+}
+
+function reviewerNote(packet, decision, findings) {
+ if (decision === "RELEASE") {
+ return `${packet.domain}: materials are traceable, authenticated, and aligned with linked assays.`;
+ }
+ const first = findings[0];
+ return `${packet.domain}: ${decision} before release because ${first.message.toLowerCase()}`;
+}
+
+function analyzePacket(input) {
+ const packet = normalizePacket(input);
+ const reviewDate = packet.reviewDate;
+ const findings = [];
+
+ if (packet.materials.length === 0) {
+ findings.push(finding(
+ "NO_MATERIALS_LISTED",
+ "major",
+ "The packet contains no research materials to authenticate.",
+ { materialCount: 0 },
+ "Add a materials table with identifiers, source, validation, and assay mappings."
+ ));
+ }
+
+ for (const material of packet.materials) {
+ findings.push(...analyzeMaterial(material, packet.assays, reviewDate));
+ }
+
+ const decision = decisionFromFindings(findings);
+ return {
+ id: packet.id,
+ title: packet.title,
+ domain: packet.domain,
+ decision,
+ confidence: confidenceFromFindings(findings),
+ materialCount: packet.materials.length,
+ assayCount: packet.assays.length,
+ findingCount: findings.length,
+ findings,
+ reviewerNote: reviewerNote(packet, decision, findings)
+ };
+}
+
+function analyzePackets(packets) {
+ const reviews = packets.map(analyzePacket);
+ const counts = reviews.reduce((acc, review) => {
+ acc[review.decision] = (acc[review.decision] || 0) + 1;
+ return acc;
+ }, {});
+
+ return {
+ generatedAt: new Date().toISOString(),
+ packetCount: reviews.length,
+ decisionCounts: {
+ RELEASE: counts.RELEASE || 0,
+ REVIEW: counts.REVIEW || 0,
+ HOLD: counts.HOLD || 0
+ },
+ reviews
+ };
+}
+
+module.exports = {
+ TRACKED_MATERIAL_TYPES,
+ STALE_VALIDATION_DAYS,
+ analyzePacket,
+ analyzePackets,
+ validRrid
+};
diff --git a/research-materials-authentication-assistant/test/research-materials-authentication-assistant.test.js b/research-materials-authentication-assistant/test/research-materials-authentication-assistant.test.js
new file mode 100644
index 00000000..c909acba
--- /dev/null
+++ b/research-materials-authentication-assistant/test/research-materials-authentication-assistant.test.js
@@ -0,0 +1,182 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const test = require("node:test");
+const { analyzePacket, analyzePackets, validRrid } = require("../src");
+
+test("validates RRID-style identifiers", () => {
+ assert.equal(validRrid("RRID:AB_123456"), true);
+ assert.equal(validRrid("RRID:CVCL_SYN1"), true);
+ assert.equal(validRrid("AB_123456"), false);
+});
+
+test("releases clean authenticated materials", () => {
+ const review = analyzePacket({
+ id: "clean",
+ title: "Clean authenticated antibody assay",
+ reviewDate: "2026-08-15",
+ materials: [
+ {
+ name: "Anti-TNF clone T1",
+ type: "antibody",
+ rrid: "RRID:AB_100001",
+ lot: "LOT-TNF-2601",
+ species: "human",
+ target: "TNF",
+ assayTargets: ["TNF"],
+ validationEvidence: ["knockdown specificity", "orthogonal ELISA"],
+ validationDate: "2026-06-01",
+ contaminationStatus: "not_detected"
+ }
+ ],
+ assays: [
+ { name: "TNF ELISA", materialName: "Anti-TNF clone T1", organism: "human", target: "TNF" }
+ ]
+ });
+
+ assert.equal(review.decision, "RELEASE");
+ assert.equal(review.findingCount, 0);
+});
+
+test("routes missing RRID, stale validation, and organism mismatch to review", () => {
+ const review = analyzePacket({
+ id: "review",
+ title: "Review material identity",
+ reviewDate: "2026-08-15",
+ materials: [
+ {
+ name: "Legacy anti-pERK",
+ type: "antibody",
+ rrid: "",
+ lot: "LOT-OLD",
+ species: "mouse",
+ claimedOrganism: "human",
+ target: "pERK",
+ assayTargets: ["pERK"],
+ validationEvidence: ["vendor blot"],
+ validationDate: "2020-01-01",
+ contaminationStatus: "not_detected"
+ }
+ ],
+ assays: [
+ { name: "Human pERK blot", materialName: "Legacy anti-pERK", organism: "human", target: "pERK" }
+ ]
+ });
+
+ assert.equal(review.decision, "REVIEW");
+ assert.ok(review.findings.some((item) => item.code === "MISSING_OR_INVALID_RRID"));
+ assert.ok(review.findings.some((item) => item.code === "STALE_VALIDATION_DATE"));
+ assert.ok(review.findings.some((item) => item.code === "ASSAY_ORGANISM_MISMATCH"));
+});
+
+test("holds contaminated cell-derived materials", () => {
+ const review = analyzePacket({
+ id: "contaminated",
+ title: "Contaminated cell stock",
+ reviewDate: "2026-08-15",
+ materials: [
+ {
+ name: "Cell stock",
+ type: "cell_line",
+ rrid: "RRID:CVCL_SYN9",
+ lot: "CELL-01",
+ species: "human",
+ validationEvidence: ["STR profile"],
+ validationDate: "2026-07-01",
+ contaminationStatus: "positive",
+ passageRange: { min: 2, max: 9 },
+ reportedPassage: 5
+ }
+ ],
+ assays: []
+ });
+
+ assert.equal(review.decision, "HOLD");
+ assert.ok(review.findings.some((item) => item.code === "CONTAMINATED_CELL_MATERIAL"));
+});
+
+test("holds antibody target mismatch and impossible passage ranges", () => {
+ const review = analyzePacket({
+ id: "hold",
+ title: "Mismatched antibody target",
+ reviewDate: "2026-08-15",
+ materials: [
+ {
+ name: "Stem cell stock",
+ type: "primary_cell",
+ rrid: "RRID:CVCL_SYN8",
+ lot: "STEM-01",
+ species: "human",
+ validationEvidence: ["identity PCR"],
+ validationDate: "2026-07-01",
+ contaminationStatus: "negative",
+ passageRange: { min: 12, max: 4 },
+ reportedPassage: 18
+ },
+ {
+ name: "Anti-GFAP",
+ type: "antibody",
+ rrid: "RRID:AB_100002",
+ lot: "AB-02",
+ species: "human",
+ target: "GFAP",
+ assayTargets: ["GFAP"],
+ validationEvidence: ["knockdown panel"],
+ validationDate: "2026-07-01",
+ contaminationStatus: "not_detected"
+ }
+ ],
+ assays: [
+ { name: "MAP2 stain", materialName: "Anti-GFAP", organism: "human", target: "MAP2" }
+ ]
+ });
+
+ assert.equal(review.decision, "HOLD");
+ assert.ok(review.findings.some((item) => item.code === "ANTIBODY_TARGET_MISMATCH"));
+ assert.ok(review.findings.some((item) => item.code === "IMPOSSIBLE_PASSAGE_RANGE"));
+});
+
+test("summarizes packet decisions deterministically", () => {
+ const report = analyzePackets([
+ {
+ id: "release",
+ title: "Release",
+ materials: [
+ {
+ name: "Anti-A",
+ type: "antibody",
+ rrid: "RRID:AB_1",
+ lot: "L1",
+ species: "human",
+ target: "A",
+ assayTargets: ["A"],
+ validationEvidence: ["specificity"],
+ validationDate: "2026-07-01",
+ contaminationStatus: "not_detected"
+ }
+ ],
+ assays: [{ name: "A", materialName: "Anti-A", organism: "human", target: "A" }]
+ },
+ {
+ id: "hold",
+ title: "Hold",
+ materials: [
+ {
+ name: "Line",
+ type: "cell_line",
+ rrid: "RRID:CVCL_1",
+ lot: "L2",
+ species: "human",
+ validationEvidence: ["STR"],
+ validationDate: "2026-07-01",
+ contaminationStatus: "detected"
+ }
+ ],
+ assays: []
+ }
+ ]);
+
+ assert.equal(report.packetCount, 2);
+ assert.equal(report.decisionCounts.RELEASE, 1);
+ assert.equal(report.decisionCounts.HOLD, 1);
+});