diff --git a/citation-retraction-evidence-guard/README.md b/citation-retraction-evidence-guard/README.md
new file mode 100644
index 00000000..52e52796
--- /dev/null
+++ b/citation-retraction-evidence-guard/README.md
@@ -0,0 +1,41 @@
+# Citation Retraction Evidence Guard
+
+Self-contained guard for SCIBASE issue #16, focused on AI-powered research assistant output safety.
+
+The module evaluates synthetic manuscript-assistant packets before auto peer review or research-gap suggestions are released. It prevents AI-generated review text from relying on retracted, contradicted, stale, unsupported, or superseded citations.
+
+## Scope
+
+- citation retraction and expression-of-concern status
+- claim-to-citation support direction
+- stale evidence windows
+- preprint-to-published-version drift
+- citation-context evidence presence
+- assistant recommendation leakage from unsafe citations
+- deterministic reviewer release decisions
+
+## Decisions
+
+- `RELEASE_ASSISTANT_OUTPUT`
+- `HOLD_FOR_EDITOR`
+- `BLOCK_ASSISTANT_OUTPUT`
+
+## Local Validation
+
+```bash
+npm test
+npm run demo
+node --check src/index.js
+node --check scripts/demo.js
+node --check test/citationRetractionEvidenceGuard.test.js
+git diff --check
+```
+
+## Boundaries
+
+- synthetic citation packets only
+- no live publisher, Crossref, PubMed, OpenAlex, or Retraction Watch calls
+- no private manuscripts
+- no credentials
+- no external AI APIs
+- no production medical or legal review claims
diff --git a/citation-retraction-evidence-guard/artifacts/citation-evidence-demo.mp4 b/citation-retraction-evidence-guard/artifacts/citation-evidence-demo.mp4
new file mode 100644
index 00000000..f89cff17
Binary files /dev/null and b/citation-retraction-evidence-guard/artifacts/citation-evidence-demo.mp4 differ
diff --git a/citation-retraction-evidence-guard/artifacts/citation-evidence-report.md b/citation-retraction-evidence-guard/artifacts/citation-evidence-report.md
new file mode 100644
index 00000000..f50814fa
--- /dev/null
+++ b/citation-retraction-evidence-guard/artifacts/citation-evidence-report.md
@@ -0,0 +1,35 @@
+# Citation Retraction Evidence Guard Report
+
+Summary: blocks=4, reviews=3, passes=1
+
+## packet:oncology-review-ready
+
+- Manuscript: ms:oncology-review-42
+- Decision: RELEASE_ASSISTANT_OUTPUT
+
+| Severity | Code | Message | Remediation |
+| --- | --- | --- | --- |
+| pass | citation_evidence_ready | Citation evidence is current, unretracted, and aligned with assistant output. | No remediation required. |
+
+## packet:neuro-preprint-review
+
+- Manuscript: ms:neuro-preprint-18
+- Decision: HOLD_FOR_EDITOR
+
+| Severity | Code | Message | Remediation |
+| --- | --- | --- | --- |
+| review | citation_superseded | cite:preprint-2020 appears superseded by cite:journal-2022. | Prefer the current published version or explain why the older version remains relevant. |
+| review | background_citation_used_as_support | cite:preprint-2020 is only background evidence for claim:biomarker-generalizes. | Attach direct support or lower the assistant confidence. |
+
+## packet:materials-block
+
+- Manuscript: ms:materials-battery-07
+- Decision: BLOCK_ASSISTANT_OUTPUT
+
+| Severity | Code | Message | Remediation |
+| --- | --- | --- | --- |
+| block | citation_retracted | cite:retracted-2017 is marked retracted. | Remove the citation from supporting evidence or rewrite the assistant note as a retraction warning. |
+| review | citation_stale_without_replication | cite:retracted-2017 is older than the configured evidence window and has no recent replication marker. | Attach a recent replication, meta-analysis, or recency caveat. |
+| block | retracted_citation_supports_claim | claim:electrolyte-stability is supported by retracted citation cite:retracted-2017. | Remove this evidence path and regenerate the assistant output. |
+| block | citation_contradicts_claim | cite:contradiction-2025 is marked as contradicting claim:electrolyte-stability. | Rewrite the assistant note to present the contradiction rather than support the claim. |
+| block | assistant_recommends_retracted_citation | Assistant recommends retracted citation cite:retracted-2017. | Regenerate the assistant note with retraction-safe evidence. |
diff --git a/citation-retraction-evidence-guard/artifacts/citation-evidence-results.json b/citation-retraction-evidence-guard/artifacts/citation-evidence-results.json
new file mode 100644
index 00000000..3490a877
--- /dev/null
+++ b/citation-retraction-evidence-guard/artifacts/citation-evidence-results.json
@@ -0,0 +1,87 @@
+{
+ "generatedAt": "2026-06-13T15:54:40.327Z",
+ "summary": {
+ "blocks": 4,
+ "reviews": 3,
+ "passes": 1
+ },
+ "results": [
+ {
+ "packetId": "packet:oncology-review-ready",
+ "manuscriptId": "ms:oncology-review-42",
+ "decision": "RELEASE_ASSISTANT_OUTPUT",
+ "findings": [
+ {
+ "severity": "pass",
+ "code": "citation_evidence_ready",
+ "message": "Citation evidence is current, unretracted, and aligned with assistant output.",
+ "remediation": "No remediation required.",
+ "ref": "packet:oncology-review-ready"
+ }
+ ]
+ },
+ {
+ "packetId": "packet:neuro-preprint-review",
+ "manuscriptId": "ms:neuro-preprint-18",
+ "decision": "HOLD_FOR_EDITOR",
+ "findings": [
+ {
+ "severity": "review",
+ "code": "citation_superseded",
+ "message": "cite:preprint-2020 appears superseded by cite:journal-2022.",
+ "remediation": "Prefer the current published version or explain why the older version remains relevant.",
+ "ref": "cite:preprint-2020"
+ },
+ {
+ "severity": "review",
+ "code": "background_citation_used_as_support",
+ "message": "cite:preprint-2020 is only background evidence for claim:biomarker-generalizes.",
+ "remediation": "Attach direct support or lower the assistant confidence.",
+ "ref": "cite:preprint-2020"
+ }
+ ]
+ },
+ {
+ "packetId": "packet:materials-block",
+ "manuscriptId": "ms:materials-battery-07",
+ "decision": "BLOCK_ASSISTANT_OUTPUT",
+ "findings": [
+ {
+ "severity": "block",
+ "code": "citation_retracted",
+ "message": "cite:retracted-2017 is marked retracted.",
+ "remediation": "Remove the citation from supporting evidence or rewrite the assistant note as a retraction warning.",
+ "ref": "cite:retracted-2017"
+ },
+ {
+ "severity": "review",
+ "code": "citation_stale_without_replication",
+ "message": "cite:retracted-2017 is older than the configured evidence window and has no recent replication marker.",
+ "remediation": "Attach a recent replication, meta-analysis, or recency caveat.",
+ "ref": "cite:retracted-2017"
+ },
+ {
+ "severity": "block",
+ "code": "retracted_citation_supports_claim",
+ "message": "claim:electrolyte-stability is supported by retracted citation cite:retracted-2017.",
+ "remediation": "Remove this evidence path and regenerate the assistant output.",
+ "ref": "cite:retracted-2017"
+ },
+ {
+ "severity": "block",
+ "code": "citation_contradicts_claim",
+ "message": "cite:contradiction-2025 is marked as contradicting claim:electrolyte-stability.",
+ "remediation": "Rewrite the assistant note to present the contradiction rather than support the claim.",
+ "ref": "cite:contradiction-2025"
+ },
+ {
+ "severity": "block",
+ "code": "assistant_recommends_retracted_citation",
+ "message": "Assistant recommends retracted citation cite:retracted-2017.",
+ "remediation": "Regenerate the assistant note with retraction-safe evidence.",
+ "ref": "cite:retracted-2017"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/citation-retraction-evidence-guard/artifacts/citation-evidence-summary.svg b/citation-retraction-evidence-guard/artifacts/citation-evidence-summary.svg
new file mode 100644
index 00000000..c4cdadc2
--- /dev/null
+++ b/citation-retraction-evidence-guard/artifacts/citation-evidence-summary.svg
@@ -0,0 +1,20 @@
+
diff --git a/citation-retraction-evidence-guard/examples/citation-evidence-packets.json b/citation-retraction-evidence-guard/examples/citation-evidence-packets.json
new file mode 100644
index 00000000..4f9cf0fa
--- /dev/null
+++ b/citation-retraction-evidence-guard/examples/citation-evidence-packets.json
@@ -0,0 +1,120 @@
+[
+ {
+ "packetId": "packet:oncology-review-ready",
+ "manuscriptId": "ms:oncology-review-42",
+ "generatedAt": "2026-06-14",
+ "claims": [
+ {
+ "id": "claim:survival-signal",
+ "text": "The intervention improves progression-free survival in the target subgroup.",
+ "evidence": [
+ {
+ "citationId": "cite:phase2-2024",
+ "supportRelation": "supports"
+ }
+ ]
+ }
+ ],
+ "citations": [
+ {
+ "id": "cite:phase2-2024",
+ "title": "Phase 2 subgroup survival analysis",
+ "doi": "10.1000/safe.2024.11",
+ "status": "active",
+ "sourceType": "journal",
+ "usedAs": "primary_support",
+ "publishedAt": "2024-04-12",
+ "hasRecentReplication": true,
+ "contextQuote": "The subgroup analysis showed improved progression-free survival under the prespecified endpoint."
+ }
+ ],
+ "assistantNote": {
+ "text": "Evidence appears aligned, recent, and unretracted for the target subgroup.",
+ "recommendedCitationIds": ["cite:phase2-2024"]
+ }
+ },
+ {
+ "packetId": "packet:neuro-preprint-review",
+ "manuscriptId": "ms:neuro-preprint-18",
+ "generatedAt": "2026-06-14",
+ "claims": [
+ {
+ "id": "claim:biomarker-generalizes",
+ "text": "The biomarker generalizes across independent neurodegeneration cohorts.",
+ "evidence": [
+ {
+ "citationId": "cite:preprint-2020",
+ "supportRelation": "background"
+ }
+ ]
+ }
+ ],
+ "citations": [
+ {
+ "id": "cite:preprint-2020",
+ "title": "Early biomarker cohort preprint",
+ "doi": "10.1101/2020.01.02.abc",
+ "status": "superseded",
+ "sourceType": "preprint",
+ "usedAs": "primary_support",
+ "publishedVersionId": "cite:journal-2022",
+ "publishedAt": "2020-01-02",
+ "hasRecentReplication": false,
+ "contextQuote": "The preliminary cohort suggested possible transfer, but the authors called for external validation."
+ }
+ ],
+ "assistantNote": {
+ "text": "The assistant should hold this claim for an editor because the cited preprint was superseded.",
+ "recommendedCitationIds": ["cite:preprint-2020"]
+ }
+ },
+ {
+ "packetId": "packet:materials-block",
+ "manuscriptId": "ms:materials-battery-07",
+ "generatedAt": "2026-06-14",
+ "claims": [
+ {
+ "id": "claim:electrolyte-stability",
+ "text": "The electrolyte is stable for 1,000 cycles under high temperature.",
+ "evidence": [
+ {
+ "citationId": "cite:retracted-2017",
+ "supportRelation": "supports"
+ },
+ {
+ "citationId": "cite:contradiction-2025",
+ "supportRelation": "contradicts"
+ }
+ ]
+ }
+ ],
+ "citations": [
+ {
+ "id": "cite:retracted-2017",
+ "title": "High-temperature electrolyte cycling",
+ "doi": "10.1000/retracted.2017.9",
+ "status": "retracted",
+ "sourceType": "journal",
+ "usedAs": "primary_support",
+ "publishedAt": "2017-09-01",
+ "hasRecentReplication": false,
+ "contextQuote": "The original article claimed stability at high temperature but was later retracted."
+ },
+ {
+ "id": "cite:contradiction-2025",
+ "title": "Independent electrolyte degradation study",
+ "doi": "10.1000/active.2025.4",
+ "status": "active",
+ "sourceType": "journal",
+ "usedAs": "primary_support",
+ "publishedAt": "2025-04-18",
+ "hasRecentReplication": true,
+ "contextQuote": "The independent study found rapid degradation under the same temperature and cycling protocol."
+ }
+ ],
+ "assistantNote": {
+ "text": "Unsafe draft: cite the older article as positive support for high-temperature stability.",
+ "recommendedCitationIds": ["cite:retracted-2017"]
+ }
+ }
+]
diff --git a/citation-retraction-evidence-guard/package.json b/citation-retraction-evidence-guard/package.json
new file mode 100644
index 00000000..7c60dc02
--- /dev/null
+++ b/citation-retraction-evidence-guard/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "citation-retraction-evidence-guard",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Synthetic citation retraction and evidence recency guard for AI research assistant outputs.",
+ "type": "commonjs",
+ "scripts": {
+ "test": "node test/citationRetractionEvidenceGuard.test.js",
+ "demo": "node scripts/demo.js"
+ }
+}
diff --git a/citation-retraction-evidence-guard/scripts/demo.js b/citation-retraction-evidence-guard/scripts/demo.js
new file mode 100644
index 00000000..8ba98ff6
--- /dev/null
+++ b/citation-retraction-evidence-guard/scripts/demo.js
@@ -0,0 +1,110 @@
+"use strict";
+
+const fs = require("node:fs");
+const os = require("node:os");
+const path = require("node:path");
+const { spawnSync } = require("node:child_process");
+const packets = require("../examples/citation-evidence-packets.json");
+const { evaluateCitationEvidenceBatch, summarize } = require("../src");
+
+const root = path.join(__dirname, "..");
+const artifactDir = path.join(root, "artifacts");
+fs.mkdirSync(artifactDir, { recursive: true });
+
+const results = evaluateCitationEvidenceBatch(packets, { asOf: "2026-06-14", maxEvidenceAgeYears: 8 });
+const summary = summarize(results);
+
+fs.writeFileSync(
+ path.join(artifactDir, "citation-evidence-results.json"),
+ JSON.stringify({ generatedAt: new Date().toISOString(), summary, results }, null, 2)
+);
+
+function reportMarkdown() {
+ const lines = [
+ "# Citation Retraction Evidence Guard Report",
+ "",
+ `Summary: blocks=${summary.blocks}, reviews=${summary.reviews}, passes=${summary.passes}`,
+ ""
+ ];
+ for (const result of results) {
+ lines.push(`## ${result.packetId}`, "");
+ lines.push(`- Manuscript: ${result.manuscriptId}`);
+ lines.push(`- Decision: ${result.decision}`, "");
+ lines.push("| Severity | Code | Message | Remediation |");
+ lines.push("| --- | --- | --- | --- |");
+ for (const finding of result.findings) {
+ lines.push(`| ${finding.severity} | ${finding.code} | ${finding.message} | ${finding.remediation} |`);
+ }
+ lines.push("");
+ }
+ return lines.join("\n").trimEnd() + "\n";
+}
+
+fs.writeFileSync(path.join(artifactDir, "citation-evidence-report.md"), reportMarkdown());
+
+function svg() {
+ const rows = results.map((result, index) => {
+ const y = 116 + index * 72;
+ const color = result.decision === "BLOCK_ASSISTANT_OUTPUT" ? "#b91c1c" : result.decision === "HOLD_FOR_EDITOR" ? "#b45309" : "#15803d";
+ return [
+ ` ${result.packetId}`,
+ ` `,
+ ` ${result.decision}`
+ ].join("\n");
+ }).join("\n");
+ return `
+`;
+}
+
+fs.writeFileSync(path.join(artifactDir, "citation-evidence-summary.svg"), svg());
+
+function writePpmFrame(file) {
+ const width = 640;
+ const height = 360;
+ const pixels = [];
+ for (let y = 0; y < height; y += 1) {
+ for (let x = 0; x < width; x += 1) {
+ let color = [248, 250, 252];
+ if (x > 70 && x < 570 && y > 80 && y < 128) color = [21, 128, 61];
+ if (x > 70 && x < 570 && y > 156 && y < 204) color = [180, 83, 9];
+ if (x > 70 && x < 570 && y > 232 && y < 280) color = [185, 28, 28];
+ pixels.push(Buffer.from(color));
+ }
+ }
+ fs.writeFileSync(file, Buffer.concat([Buffer.from(`P6\n${width} ${height}\n255\n`), ...pixels]));
+}
+
+function renderVideo() {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "citation-evidence-"));
+ const frame = path.join(tempDir, "frame.ppm");
+ const output = path.join(artifactDir, "citation-evidence-demo.mp4");
+ writePpmFrame(frame);
+ const result = spawnSync("ffmpeg", [
+ "-y",
+ "-loop", "1",
+ "-i", frame,
+ "-t", "4",
+ "-vf", "scale=640:360,format=yuv420p",
+ output
+ ], { stdio: "ignore" });
+ if (result.status !== 0) {
+ fs.writeFileSync(path.join(artifactDir, "citation-evidence-demo.txt"), "ffmpeg unavailable; SVG summary is the visual demo artifact.\n");
+ }
+}
+
+renderVideo();
+
+console.log("Citation retraction evidence guard demo generated");
+console.log(`- decisions: ${results.map((result) => `${result.packetId}:${result.decision}`).join(", ")}`);
+console.log(`- summary: ${JSON.stringify(summary)}`);
diff --git a/citation-retraction-evidence-guard/src/index.js b/citation-retraction-evidence-guard/src/index.js
new file mode 100644
index 00000000..3f151cc1
--- /dev/null
+++ b/citation-retraction-evidence-guard/src/index.js
@@ -0,0 +1,310 @@
+"use strict";
+
+const RELEASE = "RELEASE_ASSISTANT_OUTPUT";
+const HOLD = "HOLD_FOR_EDITOR";
+const BLOCK = "BLOCK_ASSISTANT_OUTPUT";
+
+function asDate(value, field, findings, severity = "review") {
+ if (!value) {
+ findings.push({
+ severity,
+ code: `${field}_missing`,
+ message: `${field} is missing.`,
+ remediation: `Attach ${field} evidence before release.`
+ });
+ return null;
+ }
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ findings.push({
+ severity: "block",
+ code: `${field}_invalid`,
+ message: `${field} is not a valid date: ${value}.`,
+ remediation: `Normalize ${field} to ISO-8601.`
+ });
+ return null;
+ }
+ return date;
+}
+
+function yearsBetween(older, newer) {
+ return (newer.getTime() - older.getTime()) / (365.25 * 24 * 60 * 60 * 1000);
+}
+
+function normalizeList(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function addFinding(findings, severity, code, message, remediation, ref) {
+ findings.push({ severity, code, message, remediation, ref });
+}
+
+function indexCitations(citations) {
+ return new Map(normalizeList(citations).map((citation) => [citation.id, citation]));
+}
+
+function evaluateCitation(citation, packet, options, findings) {
+ const now = asDate(options.asOf || packet.generatedAt || "2026-06-14", "asOf", findings, "block");
+ const publishedAt = asDate(citation.publishedAt, "publishedAt", findings);
+
+ if (!citation.id || !citation.title || !citation.doi) {
+ addFinding(
+ findings,
+ "review",
+ "citation_identity_incomplete",
+ `Citation identity is incomplete for ${citation.id || "unknown citation"}.`,
+ "Attach stable citation id, title, and DOI before assistant output release.",
+ citation.id
+ );
+ }
+
+ if (citation.status === "retracted") {
+ addFinding(
+ findings,
+ "block",
+ "citation_retracted",
+ `${citation.id} is marked retracted.`,
+ "Remove the citation from supporting evidence or rewrite the assistant note as a retraction warning.",
+ citation.id
+ );
+ }
+
+ if (citation.status === "expression_of_concern") {
+ addFinding(
+ findings,
+ "review",
+ "citation_expression_of_concern",
+ `${citation.id} has an expression of concern.`,
+ "Route to an editor before AI review output cites this source as support.",
+ citation.id
+ );
+ }
+
+ if (citation.status === "superseded" || citation.publishedVersionId) {
+ addFinding(
+ findings,
+ "review",
+ "citation_superseded",
+ `${citation.id} appears superseded by ${citation.publishedVersionId || "a newer version"}.`,
+ "Prefer the current published version or explain why the older version remains relevant.",
+ citation.id
+ );
+ }
+
+ if (citation.sourceType === "preprint" && !citation.publishedVersionId && citation.usedAs === "primary_support") {
+ addFinding(
+ findings,
+ "review",
+ "primary_preprint_without_published_version_check",
+ `${citation.id} is a primary-support preprint without a published-version check.`,
+ "Record a published-version search or downgrade the assistant confidence.",
+ citation.id
+ );
+ }
+
+ if (now && publishedAt && yearsBetween(publishedAt, now) > (options.maxEvidenceAgeYears || 8) && !citation.hasRecentReplication) {
+ addFinding(
+ findings,
+ "review",
+ "citation_stale_without_replication",
+ `${citation.id} is older than the configured evidence window and has no recent replication marker.`,
+ "Attach a recent replication, meta-analysis, or recency caveat.",
+ citation.id
+ );
+ }
+
+ if (!citation.contextQuote || citation.contextQuote.length < 24) {
+ addFinding(
+ findings,
+ "review",
+ "citation_context_missing",
+ `${citation.id} lacks a usable citation-context quote.`,
+ "Attach the local evidence sentence used by the assistant.",
+ citation.id
+ );
+ }
+}
+
+function evaluateClaim(claim, citationMap, findings) {
+ const evidence = normalizeList(claim.evidence);
+ if (!claim.id || !claim.text) {
+ addFinding(
+ findings,
+ "block",
+ "claim_identity_missing",
+ "A claim is missing id or text.",
+ "Attach stable claim id and text before review output release.",
+ claim.id
+ );
+ }
+
+ if (evidence.length === 0) {
+ addFinding(
+ findings,
+ "block",
+ "claim_without_evidence",
+ `${claim.id} has no citation evidence.`,
+ "Attach at least one citation or remove the assistant claim.",
+ claim.id
+ );
+ }
+
+ for (const item of evidence) {
+ const citation = citationMap.get(item.citationId);
+ if (!citation) {
+ addFinding(
+ findings,
+ "block",
+ "claim_evidence_missing_citation",
+ `${claim.id} references missing citation ${item.citationId}.`,
+ "Add the cited source packet or remove the evidence reference.",
+ item.citationId
+ );
+ continue;
+ }
+
+ if (item.supportRelation === "contradicts") {
+ addFinding(
+ findings,
+ "block",
+ "citation_contradicts_claim",
+ `${citation.id} is marked as contradicting ${claim.id}.`,
+ "Rewrite the assistant note to present the contradiction rather than support the claim.",
+ citation.id
+ );
+ }
+
+ if (item.supportRelation === "background") {
+ addFinding(
+ findings,
+ "review",
+ "background_citation_used_as_support",
+ `${citation.id} is only background evidence for ${claim.id}.`,
+ "Attach direct support or lower the assistant confidence.",
+ citation.id
+ );
+ }
+
+ if (citation.status === "retracted" && item.supportRelation === "supports") {
+ addFinding(
+ findings,
+ "block",
+ "retracted_citation_supports_claim",
+ `${claim.id} is supported by retracted citation ${citation.id}.`,
+ "Remove this evidence path and regenerate the assistant output.",
+ citation.id
+ );
+ }
+ }
+}
+
+function evaluateAssistantNote(note, citationMap, findings) {
+ if (!note || !note.text) {
+ addFinding(
+ findings,
+ "review",
+ "assistant_note_missing",
+ "Assistant note text is missing.",
+ "Generate reviewer-visible text only after the evidence packet is complete.",
+ "assistantNote"
+ );
+ return;
+ }
+
+ for (const citationId of normalizeList(note.recommendedCitationIds)) {
+ const citation = citationMap.get(citationId);
+ if (!citation) {
+ addFinding(
+ findings,
+ "block",
+ "assistant_recommends_missing_citation",
+ `Assistant recommends missing citation ${citationId}.`,
+ "Remove the recommendation or attach the cited source packet.",
+ citationId
+ );
+ continue;
+ }
+ if (citation.status === "retracted") {
+ addFinding(
+ findings,
+ "block",
+ "assistant_recommends_retracted_citation",
+ `Assistant recommends retracted citation ${citationId}.`,
+ "Regenerate the assistant note with retraction-safe evidence.",
+ citationId
+ );
+ }
+ }
+}
+
+function decide(findings) {
+ if (findings.some((finding) => finding.severity === "block")) return BLOCK;
+ if (findings.some((finding) => finding.severity === "review")) return HOLD;
+ return RELEASE;
+}
+
+function evaluateCitationEvidencePacket(packet, options = {}) {
+ const findings = [];
+ if (!packet || typeof packet !== "object") {
+ throw new TypeError("packet must be an object");
+ }
+ if (!packet.packetId || !packet.manuscriptId) {
+ addFinding(
+ findings,
+ "block",
+ "packet_identity_missing",
+ "Packet id or manuscript id is missing.",
+ "Attach packetId and manuscriptId before release.",
+ "packet"
+ );
+ }
+
+ const citations = normalizeList(packet.citations);
+ const citationMap = indexCitations(citations);
+ for (const citation of citations) evaluateCitation(citation, packet, options, findings);
+ for (const claim of normalizeList(packet.claims)) evaluateClaim(claim, citationMap, findings);
+ evaluateAssistantNote(packet.assistantNote, citationMap, findings);
+
+ if (findings.length === 0) {
+ addFinding(
+ findings,
+ "pass",
+ "citation_evidence_ready",
+ "Citation evidence is current, unretracted, and aligned with assistant output.",
+ "No remediation required.",
+ packet.packetId
+ );
+ }
+
+ return {
+ packetId: packet.packetId,
+ manuscriptId: packet.manuscriptId,
+ decision: decide(findings),
+ findings
+ };
+}
+
+function evaluateCitationEvidenceBatch(packets, options = {}) {
+ return normalizeList(packets).map((packet) => evaluateCitationEvidencePacket(packet, options));
+}
+
+function summarize(results) {
+ const summary = { blocks: 0, reviews: 0, passes: 0 };
+ for (const result of normalizeList(results)) {
+ for (const finding of result.findings) {
+ if (finding.severity === "block") summary.blocks += 1;
+ if (finding.severity === "review") summary.reviews += 1;
+ if (finding.severity === "pass") summary.passes += 1;
+ }
+ }
+ return summary;
+}
+
+module.exports = {
+ RELEASE,
+ HOLD,
+ BLOCK,
+ evaluateCitationEvidencePacket,
+ evaluateCitationEvidenceBatch,
+ summarize
+};
diff --git a/citation-retraction-evidence-guard/test/citationRetractionEvidenceGuard.test.js b/citation-retraction-evidence-guard/test/citationRetractionEvidenceGuard.test.js
new file mode 100644
index 00000000..735a6486
--- /dev/null
+++ b/citation-retraction-evidence-guard/test/citationRetractionEvidenceGuard.test.js
@@ -0,0 +1,59 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const packets = require("../examples/citation-evidence-packets.json");
+const {
+ RELEASE,
+ HOLD,
+ BLOCK,
+ evaluateCitationEvidencePacket,
+ evaluateCitationEvidenceBatch,
+ summarize
+} = require("../src");
+
+const results = evaluateCitationEvidenceBatch(packets, { asOf: "2026-06-14", maxEvidenceAgeYears: 8 });
+
+assert.equal(results[0].decision, RELEASE);
+assert.deepEqual(
+ results[0].findings.map((finding) => finding.code),
+ ["citation_evidence_ready"]
+);
+
+assert.equal(results[1].decision, HOLD);
+assert(results[1].findings.some((finding) => finding.code === "citation_superseded"));
+assert(results[1].findings.some((finding) => finding.code === "background_citation_used_as_support"));
+
+assert.equal(results[2].decision, BLOCK);
+assert(results[2].findings.some((finding) => finding.code === "citation_retracted"));
+assert(results[2].findings.some((finding) => finding.code === "retracted_citation_supports_claim"));
+assert(results[2].findings.some((finding) => finding.code === "citation_contradicts_claim"));
+assert(results[2].findings.some((finding) => finding.code === "assistant_recommends_retracted_citation"));
+
+const missingCitation = evaluateCitationEvidencePacket({
+ packetId: "packet:missing-citation",
+ manuscriptId: "ms:missing-citation",
+ generatedAt: "2026-06-14",
+ claims: [
+ {
+ id: "claim:unsupported",
+ text: "Unsupported claim.",
+ evidence: [{ citationId: "cite:missing", supportRelation: "supports" }]
+ }
+ ],
+ citations: [],
+ assistantNote: {
+ text: "Recommend a missing source.",
+ recommendedCitationIds: ["cite:missing"]
+ }
+});
+
+assert.equal(missingCitation.decision, BLOCK);
+assert(missingCitation.findings.some((finding) => finding.code === "claim_evidence_missing_citation"));
+assert(missingCitation.findings.some((finding) => finding.code === "assistant_recommends_missing_citation"));
+
+const summary = summarize(results);
+assert.equal(summary.blocks, 4);
+assert.equal(summary.reviews, 3);
+assert.equal(summary.passes, 1);
+
+console.log("citation retraction evidence guard tests passed");