diff --git a/orcid-publication-claim-guard/README.md b/orcid-publication-claim-guard/README.md
new file mode 100644
index 00000000..378e2f94
--- /dev/null
+++ b/orcid-publication-claim-guard/README.md
@@ -0,0 +1,33 @@
+# ORCID Publication Claim Guard
+
+Self-contained reviewer artifact for the User & Project Management bounty.
+
+This module checks whether publications imported from ORCID, DOI metadata, or
+profile sync jobs should be attached to a researcher profile automatically,
+held for human review, or quarantined as a likely wrong-person claim.
+
+## Scope
+
+- Match imported publications to a researcher profile by ORCID, normalized name,
+ affiliation, coauthor overlap, and DOI ownership.
+- Detect ambiguous homonyms, DOI conflicts, retracted or withdrawn works, and
+ weak evidence claims before they affect profile activity or reputation metrics.
+- Produce deterministic audit evidence for reviewer and institution records.
+- Use synthetic data only. No identity-provider calls, credentials, live ORCID
+ records, private researcher data, or external APIs.
+
+## Commands
+
+```bash
+node orcid-publication-claim-guard/test.js
+node orcid-publication-claim-guard/demo.js
+node orcid-publication-claim-guard/render-video.js
+node --check orcid-publication-claim-guard/index.js
+node --check orcid-publication-claim-guard/sample-data.js
+node --check orcid-publication-claim-guard/test.js
+node --check orcid-publication-claim-guard/demo.js
+node --check orcid-publication-claim-guard/render-video.js
+```
+
+The demo writes reviewer-ready JSON, Markdown, SVG, and MP4 artifacts under
+`orcid-publication-claim-guard/reports/`.
diff --git a/orcid-publication-claim-guard/demo.js b/orcid-publication-claim-guard/demo.js
new file mode 100644
index 00000000..dd44665f
--- /dev/null
+++ b/orcid-publication-claim-guard/demo.js
@@ -0,0 +1,38 @@
+const fs = require("fs");
+const path = require("path");
+const {
+ evaluatePublicationClaims,
+ renderMarkdownReport,
+ renderSvgReport,
+} = require("./index");
+const {
+ existingClaims,
+ importedPublications,
+ researchers,
+} = require("./sample-data");
+
+const reportDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportDir, { recursive: true });
+
+const report = evaluatePublicationClaims({
+ researcher: researchers[0],
+ publications: importedPublications,
+ existingClaims,
+});
+
+fs.writeFileSync(
+ path.join(reportDir, "orcid-publication-claim-review.json"),
+ `${JSON.stringify(report, null, 2)}\n`
+);
+fs.writeFileSync(
+ path.join(reportDir, "orcid-publication-claim-review.md"),
+ `${renderMarkdownReport(report)}\n`
+);
+fs.writeFileSync(
+ path.join(reportDir, "orcid-publication-claim-review.svg"),
+ renderSvgReport(report)
+);
+
+console.log(
+ `wrote ${report.summary.total} publication decisions to ${path.relative(process.cwd(), reportDir)}`
+);
diff --git a/orcid-publication-claim-guard/index.js b/orcid-publication-claim-guard/index.js
new file mode 100644
index 00000000..202a050f
--- /dev/null
+++ b/orcid-publication-claim-guard/index.js
@@ -0,0 +1,292 @@
+const crypto = require("crypto");
+
+function normalizeText(value) {
+ return String(value || "")
+ .toLowerCase()
+ .normalize("NFKD")
+ .replace(/[\u0300-\u036f]/g, "")
+ .replace(/[^a-z0-9]+/g, " ")
+ .trim();
+}
+
+function normalizeDoi(value) {
+ return String(value || "")
+ .trim()
+ .toLowerCase()
+ .replace(/^https?:\/\/(dx\.)?doi\.org\//, "");
+}
+
+function tokenSet(value) {
+ return new Set(normalizeText(value).split(" ").filter(Boolean));
+}
+
+function overlapScore(left, right) {
+ const a = tokenSet(left);
+ const b = tokenSet(right);
+ if (a.size === 0 || b.size === 0) return 0;
+
+ let overlap = 0;
+ for (const token of a) {
+ if (b.has(token)) overlap += 1;
+ }
+ return overlap / Math.max(a.size, b.size);
+}
+
+function nameMatches(researcher, author) {
+ const names = [researcher.displayName, ...(researcher.aliases || [])];
+ return names.some((name) => overlapScore(name, author.name) >= 0.8);
+}
+
+function affiliationMatches(researcher, affiliation) {
+ return (researcher.affiliations || []).some(
+ (item) => overlapScore(item, affiliation) >= 0.55
+ );
+}
+
+function coauthorMatches(researcher, publication) {
+ const known = new Set(
+ (researcher.knownCoauthors || []).map((name) => normalizeText(name))
+ );
+ return (publication.authors || [])
+ .map((author) => normalizeText(author.name))
+ .filter((name) => known.has(name));
+}
+
+function findResearcherAuthor(researcher, publication) {
+ return (publication.authors || []).find((author) => {
+ if (author.orcid && researcher.orcid && author.orcid === researcher.orcid) {
+ return true;
+ }
+ return nameMatches(researcher, author);
+ });
+}
+
+function findConflictingOwner(researcher, publication, existingClaims) {
+ const doi = normalizeDoi(publication.doi);
+ return existingClaims.find(
+ (claim) =>
+ normalizeDoi(claim.doi) === doi && claim.researcherId !== researcher.id
+ );
+}
+
+function scorePublicationClaim(researcher, publication, existingClaims = []) {
+ const evidence = [];
+ const blockers = [];
+ const actions = [];
+ let score = 0;
+
+ const matchedAuthor = findResearcherAuthor(researcher, publication);
+ const conflictingOwner = findConflictingOwner(
+ researcher,
+ publication,
+ existingClaims
+ );
+
+ if (conflictingOwner) {
+ blockers.push(
+ `DOI already claimed by ${conflictingOwner.researcherId}: ${conflictingOwner.doi}`
+ );
+ actions.push("quarantine duplicate DOI until the existing owner is reviewed");
+ }
+
+ if (publication.status && publication.status !== "published") {
+ blockers.push(`publication status is ${publication.status}`);
+ actions.push("hold non-published or retracted work out of profile metrics");
+ }
+
+ if (!matchedAuthor) {
+ blockers.push("no author row matches the researcher name or ORCID");
+ actions.push("request manual attribution evidence before profile attach");
+ } else {
+ if (matchedAuthor.orcid === researcher.orcid) {
+ score += 45;
+ evidence.push("exact ORCID match on an author row");
+ } else if (matchedAuthor.orcid && matchedAuthor.orcid !== researcher.orcid) {
+ blockers.push(
+ `matching name has different ORCID ${matchedAuthor.orcid}`
+ );
+ actions.push("treat as homonym until the alternate ORCID is reconciled");
+ } else {
+ score += 18;
+ evidence.push("normalized author name matches profile name or alias");
+ }
+
+ if (affiliationMatches(researcher, matchedAuthor.affiliation)) {
+ score += 18;
+ evidence.push("author affiliation matches researcher profile");
+ } else if (matchedAuthor.affiliation) {
+ blockers.push(
+ `author affiliation '${matchedAuthor.affiliation}' is not in profile`
+ );
+ actions.push("ask researcher to confirm affiliation at publication time");
+ }
+ }
+
+ const coauthors = coauthorMatches(researcher, publication);
+ if (coauthors.length > 0) {
+ score += Math.min(18, coauthors.length * 9);
+ evidence.push(`known coauthor overlap: ${coauthors.join(", ")}`);
+ }
+
+ if (
+ publication.year < researcher.profileWindow.start ||
+ publication.year > researcher.profileWindow.end
+ ) {
+ blockers.push(`publication year ${publication.year} is outside profile window`);
+ actions.push("verify historic affiliation before attaching");
+ } else {
+ score += 8;
+ evidence.push("publication year falls inside profile window");
+ }
+
+ if (publication.doi) {
+ score += 6;
+ evidence.push(`normalized DOI: ${normalizeDoi(publication.doi)}`);
+ }
+
+ let decision = "review";
+ if (blockers.length > 0) {
+ decision = "quarantine";
+ } else if (score >= 72) {
+ decision = "auto_accept";
+ actions.push("attach to profile and include in reputation metrics");
+ } else {
+ actions.push("hold for reviewer approval before affecting metrics");
+ }
+
+ const result = {
+ researcherId: researcher.id,
+ publicationId: publication.id,
+ title: publication.title,
+ doi: normalizeDoi(publication.doi),
+ decision,
+ score,
+ evidence,
+ blockers,
+ actions: [...new Set(actions)],
+ };
+
+ return {
+ ...result,
+ auditDigest: digestResult(result),
+ };
+}
+
+function digestResult(result) {
+ return crypto
+ .createHash("sha256")
+ .update(JSON.stringify(result))
+ .digest("hex");
+}
+
+function evaluatePublicationClaims({
+ researcher,
+ publications,
+ existingClaims = [],
+}) {
+ const decisions = publications.map((publication) =>
+ scorePublicationClaim(researcher, publication, existingClaims)
+ );
+ const summary = decisions.reduce(
+ (acc, decision) => {
+ acc.total += 1;
+ acc[decision.decision] += 1;
+ return acc;
+ },
+ { total: 0, auto_accept: 0, review: 0, quarantine: 0 }
+ );
+
+ return {
+ generatedAt: new Date().toISOString(),
+ researcher: {
+ id: researcher.id,
+ displayName: researcher.displayName,
+ orcid: researcher.orcid,
+ },
+ summary,
+ decisions,
+ };
+}
+
+function renderMarkdownReport(report) {
+ const lines = [
+ "# ORCID Publication Claim Guard Report",
+ "",
+ `Researcher: ${report.researcher.displayName} (${report.researcher.orcid})`,
+ `Generated: ${report.generatedAt}`,
+ "",
+ "## Summary",
+ "",
+ `- Total imports checked: ${report.summary.total}`,
+ `- Auto accepted: ${report.summary.auto_accept}`,
+ `- Needs review: ${report.summary.review}`,
+ `- Quarantined: ${report.summary.quarantine}`,
+ "",
+ "## Decisions",
+ "",
+ ];
+
+ for (const item of report.decisions) {
+ lines.push(`### ${item.publicationId} - ${item.decision}`);
+ lines.push("");
+ lines.push(`- Title: ${item.title}`);
+ lines.push(`- DOI: ${item.doi}`);
+ lines.push(`- Score: ${item.score}`);
+ lines.push(`- Audit digest: ${item.auditDigest}`);
+ lines.push(`- Evidence: ${item.evidence.join("; ") || "none"}`);
+ lines.push(`- Blockers: ${item.blockers.join("; ") || "none"}`);
+ lines.push(`- Actions: ${item.actions.join("; ")}`);
+ lines.push("");
+ }
+
+ return lines.join("\n");
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function renderSvgReport(report) {
+ const rows = report.decisions
+ .map((item, index) => {
+ const y = 190 + index * 86;
+ const color =
+ item.decision === "auto_accept"
+ ? "#059669"
+ : item.decision === "review"
+ ? "#d97706"
+ : "#dc2626";
+ return `
+
+
+
+ ${escapeXml(item.publicationId)} - ${escapeXml(item.decision)}
+ ${escapeXml(item.title.slice(0, 104))}
+ score ${item.score}
+ `;
+ })
+ .join("");
+
+ return `
+`;
+}
+
+module.exports = {
+ evaluatePublicationClaims,
+ normalizeDoi,
+ normalizeText,
+ renderMarkdownReport,
+ renderSvgReport,
+ scorePublicationClaim,
+};
diff --git a/orcid-publication-claim-guard/render-video.js b/orcid-publication-claim-guard/render-video.js
new file mode 100644
index 00000000..a8a9261e
--- /dev/null
+++ b/orcid-publication-claim-guard/render-video.js
@@ -0,0 +1,51 @@
+const fs = require("fs");
+const path = require("path");
+const { execFileSync } = require("child_process");
+
+const reportDir = path.join(__dirname, "reports");
+const jsonPath = path.join(reportDir, "orcid-publication-claim-review.json");
+const outputPath = path.join(reportDir, "demo.mp4");
+const svgPath = path.join(reportDir, "orcid-publication-claim-review.svg");
+const framePath = path.join(reportDir, "orcid-publication-claim-review.png");
+
+if (!fs.existsSync(jsonPath)) {
+ require("./demo");
+}
+
+const report = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
+if (!fs.existsSync(svgPath)) {
+ throw new Error(`missing SVG report: ${svgPath}`);
+}
+
+execFileSync(
+ "rsvg-convert",
+ ["--width", "1280", "--height", "720", "--output", framePath, svgPath],
+ { stdio: "inherit" }
+);
+
+execFileSync(
+ "ffmpeg",
+ [
+ "-y",
+ "-loop",
+ "1",
+ "-framerate",
+ "25",
+ "-i",
+ framePath,
+ "-t",
+ "4",
+ "-pix_fmt",
+ "yuv420p",
+ "-vf",
+ "scale=1280:720",
+ "-movflags",
+ "+faststart",
+ outputPath,
+ ],
+ { stdio: "inherit" }
+);
+
+console.log(
+ `wrote ${path.relative(process.cwd(), outputPath)} for ${report.summary.total} checked imports`
+);
diff --git a/orcid-publication-claim-guard/reports/demo.mp4 b/orcid-publication-claim-guard/reports/demo.mp4
new file mode 100644
index 00000000..7cdf6c2e
Binary files /dev/null and b/orcid-publication-claim-guard/reports/demo.mp4 differ
diff --git a/orcid-publication-claim-guard/reports/orcid-publication-claim-review.json b/orcid-publication-claim-guard/reports/orcid-publication-claim-review.json
new file mode 100644
index 00000000..508ade8c
--- /dev/null
+++ b/orcid-publication-claim-guard/reports/orcid-publication-claim-review.json
@@ -0,0 +1,121 @@
+{
+ "generatedAt": "2026-05-31T06:40:07.465Z",
+ "researcher": {
+ "id": "researcher-ada-kim",
+ "displayName": "Ada Kim",
+ "orcid": "0000-0002-1825-0097"
+ },
+ "summary": {
+ "total": 5,
+ "auto_accept": 1,
+ "review": 1,
+ "quarantine": 3
+ },
+ "decisions": [
+ {
+ "researcherId": "researcher-ada-kim",
+ "publicationId": "pub-exact-orcid",
+ "title": "Reproducible notebooks for kinase pathway triage",
+ "doi": "10.5555/northbridge.2025.042",
+ "decision": "auto_accept",
+ "score": 86,
+ "evidence": [
+ "exact ORCID match on an author row",
+ "author affiliation matches researcher profile",
+ "known coauthor overlap: mina patel",
+ "publication year falls inside profile window",
+ "normalized DOI: 10.5555/northbridge.2025.042"
+ ],
+ "blockers": [],
+ "actions": [
+ "attach to profile and include in reputation metrics"
+ ],
+ "auditDigest": "fb4879a5f58f01e8166d49170546a7fffb452782cbb1cffe750f9dafe22ac106"
+ },
+ {
+ "researcherId": "researcher-ada-kim",
+ "publicationId": "pub-homonym-orcid-mismatch",
+ "title": "Hydrogen electrolyzer scheduling in microgrids",
+ "doi": "10.7777/westlake.2026.014",
+ "decision": "quarantine",
+ "score": 14,
+ "evidence": [
+ "publication year falls inside profile window",
+ "normalized DOI: 10.7777/westlake.2026.014"
+ ],
+ "blockers": [
+ "matching name has different ORCID 0000-0003-4444-7788",
+ "author affiliation 'Westlake Energy Systems' is not in profile"
+ ],
+ "actions": [
+ "treat as homonym until the alternate ORCID is reconciled",
+ "ask researcher to confirm affiliation at publication time"
+ ],
+ "auditDigest": "6091cef81158046c74cd975865b92a7c6e5bf2e79f75e432b0f36ad4901575bf"
+ },
+ {
+ "researcherId": "researcher-ada-kim",
+ "publicationId": "pub-weak-but-reviewable",
+ "title": "Sparse pathway graphs for reproducibility audits",
+ "doi": "10.5555/northbridge.2026.081",
+ "decision": "review",
+ "score": 59,
+ "evidence": [
+ "normalized author name matches profile name or alias",
+ "author affiliation matches researcher profile",
+ "known coauthor overlap: grace lee",
+ "publication year falls inside profile window",
+ "normalized DOI: 10.5555/northbridge.2026.081"
+ ],
+ "blockers": [],
+ "actions": [
+ "hold for reviewer approval before affecting metrics"
+ ],
+ "auditDigest": "0da5f48db0ec6ce9f5a6f9abeefb7b2254df777d8bf85c7ede618790e83e30fb"
+ },
+ {
+ "researcherId": "researcher-ada-kim",
+ "publicationId": "pub-duplicate-doi",
+ "title": "Grid-scale battery dispatch under volatile demand",
+ "doi": "10.7777/westlake.2024.009",
+ "decision": "quarantine",
+ "score": 32,
+ "evidence": [
+ "normalized author name matches profile name or alias",
+ "publication year falls inside profile window",
+ "normalized DOI: 10.7777/westlake.2024.009"
+ ],
+ "blockers": [
+ "DOI already claimed by researcher-ada-kim-energy: 10.7777/westlake.2024.009",
+ "author affiliation 'Westlake Energy Systems' is not in profile"
+ ],
+ "actions": [
+ "quarantine duplicate DOI until the existing owner is reviewed",
+ "ask researcher to confirm affiliation at publication time"
+ ],
+ "auditDigest": "34a98c869c817ce327a67f538283d188787e86da2b21be6fd5d3c0fa20413799"
+ },
+ {
+ "researcherId": "researcher-ada-kim",
+ "publicationId": "pub-retracted",
+ "title": "Automated profile scoring from unreconciled citations",
+ "doi": "10.9999/example.2022.100",
+ "decision": "quarantine",
+ "score": 59,
+ "evidence": [
+ "normalized author name matches profile name or alias",
+ "author affiliation matches researcher profile",
+ "known coauthor overlap: omar singh",
+ "publication year falls inside profile window",
+ "normalized DOI: 10.9999/example.2022.100"
+ ],
+ "blockers": [
+ "publication status is retracted"
+ ],
+ "actions": [
+ "hold non-published or retracted work out of profile metrics"
+ ],
+ "auditDigest": "57b56560faf9b3e7319ac2ed2050123164d26c77aff9637167af98977355bfd3"
+ }
+ ]
+}
diff --git a/orcid-publication-claim-guard/reports/orcid-publication-claim-review.md b/orcid-publication-claim-guard/reports/orcid-publication-claim-review.md
new file mode 100644
index 00000000..9e1c3d7f
--- /dev/null
+++ b/orcid-publication-claim-guard/reports/orcid-publication-claim-review.md
@@ -0,0 +1,64 @@
+# ORCID Publication Claim Guard Report
+
+Researcher: Ada Kim (0000-0002-1825-0097)
+Generated: 2026-05-31T06:40:07.465Z
+
+## Summary
+
+- Total imports checked: 5
+- Auto accepted: 1
+- Needs review: 1
+- Quarantined: 3
+
+## Decisions
+
+### pub-exact-orcid - auto_accept
+
+- Title: Reproducible notebooks for kinase pathway triage
+- DOI: 10.5555/northbridge.2025.042
+- Score: 86
+- Audit digest: fb4879a5f58f01e8166d49170546a7fffb452782cbb1cffe750f9dafe22ac106
+- Evidence: exact ORCID match on an author row; author affiliation matches researcher profile; known coauthor overlap: mina patel; publication year falls inside profile window; normalized DOI: 10.5555/northbridge.2025.042
+- Blockers: none
+- Actions: attach to profile and include in reputation metrics
+
+### pub-homonym-orcid-mismatch - quarantine
+
+- Title: Hydrogen electrolyzer scheduling in microgrids
+- DOI: 10.7777/westlake.2026.014
+- Score: 14
+- Audit digest: 6091cef81158046c74cd975865b92a7c6e5bf2e79f75e432b0f36ad4901575bf
+- Evidence: publication year falls inside profile window; normalized DOI: 10.7777/westlake.2026.014
+- Blockers: matching name has different ORCID 0000-0003-4444-7788; author affiliation 'Westlake Energy Systems' is not in profile
+- Actions: treat as homonym until the alternate ORCID is reconciled; ask researcher to confirm affiliation at publication time
+
+### pub-weak-but-reviewable - review
+
+- Title: Sparse pathway graphs for reproducibility audits
+- DOI: 10.5555/northbridge.2026.081
+- Score: 59
+- Audit digest: 0da5f48db0ec6ce9f5a6f9abeefb7b2254df777d8bf85c7ede618790e83e30fb
+- Evidence: normalized author name matches profile name or alias; author affiliation matches researcher profile; known coauthor overlap: grace lee; publication year falls inside profile window; normalized DOI: 10.5555/northbridge.2026.081
+- Blockers: none
+- Actions: hold for reviewer approval before affecting metrics
+
+### pub-duplicate-doi - quarantine
+
+- Title: Grid-scale battery dispatch under volatile demand
+- DOI: 10.7777/westlake.2024.009
+- Score: 32
+- Audit digest: 34a98c869c817ce327a67f538283d188787e86da2b21be6fd5d3c0fa20413799
+- Evidence: normalized author name matches profile name or alias; publication year falls inside profile window; normalized DOI: 10.7777/westlake.2024.009
+- Blockers: DOI already claimed by researcher-ada-kim-energy: 10.7777/westlake.2024.009; author affiliation 'Westlake Energy Systems' is not in profile
+- Actions: quarantine duplicate DOI until the existing owner is reviewed; ask researcher to confirm affiliation at publication time
+
+### pub-retracted - quarantine
+
+- Title: Automated profile scoring from unreconciled citations
+- DOI: 10.9999/example.2022.100
+- Score: 59
+- Audit digest: 57b56560faf9b3e7319ac2ed2050123164d26c77aff9637167af98977355bfd3
+- Evidence: normalized author name matches profile name or alias; author affiliation matches researcher profile; known coauthor overlap: omar singh; publication year falls inside profile window; normalized DOI: 10.9999/example.2022.100
+- Blockers: publication status is retracted
+- Actions: hold non-published or retracted work out of profile metrics
+
diff --git a/orcid-publication-claim-guard/reports/orcid-publication-claim-review.svg b/orcid-publication-claim-guard/reports/orcid-publication-claim-review.svg
new file mode 100644
index 00000000..caab1903
--- /dev/null
+++ b/orcid-publication-claim-guard/reports/orcid-publication-claim-review.svg
@@ -0,0 +1,44 @@
+
+
\ No newline at end of file
diff --git a/orcid-publication-claim-guard/sample-data.js b/orcid-publication-claim-guard/sample-data.js
new file mode 100644
index 00000000..bb612a1d
--- /dev/null
+++ b/orcid-publication-claim-guard/sample-data.js
@@ -0,0 +1,117 @@
+const researchers = [
+ {
+ id: "researcher-ada-kim",
+ orcid: "0000-0002-1825-0097",
+ displayName: "Ada Kim",
+ aliases: ["A. Kim", "Ada M. Kim"],
+ affiliations: ["Northbridge Institute", "Northbridge Bioinformatics Lab"],
+ domains: ["northbridge.example"],
+ knownCoauthors: ["Mina Patel", "Omar Singh", "Grace Lee"],
+ profileWindow: { start: 2019, end: 2026 },
+ },
+ {
+ id: "researcher-ada-kim-energy",
+ orcid: "0000-0003-4444-7788",
+ displayName: "Ada Kim",
+ aliases: ["A. Kim"],
+ affiliations: ["Westlake Energy Systems"],
+ domains: ["westlake.example"],
+ knownCoauthors: ["Evan Stone", "Priya Raman"],
+ profileWindow: { start: 2021, end: 2026 },
+ },
+];
+
+const existingClaims = [
+ {
+ doi: "10.5555/northbridge.2025.042",
+ researcherId: "researcher-ada-kim",
+ title: "Reproducible notebooks for kinase pathway triage",
+ },
+ {
+ doi: "10.7777/westlake.2024.009",
+ researcherId: "researcher-ada-kim-energy",
+ title: "Grid-scale battery dispatch under volatile demand",
+ },
+];
+
+const importedPublications = [
+ {
+ id: "pub-exact-orcid",
+ title: "Reproducible notebooks for kinase pathway triage",
+ doi: "10.5555/northbridge.2025.042",
+ year: 2025,
+ venue: "Journal of Computational Biology",
+ status: "published",
+ source: "orcid-sync",
+ authors: [
+ {
+ name: "Ada M. Kim",
+ orcid: "0000-0002-1825-0097",
+ affiliation: "Northbridge Bioinformatics Lab",
+ },
+ { name: "Mina Patel", affiliation: "Northbridge Institute" },
+ ],
+ },
+ {
+ id: "pub-homonym-orcid-mismatch",
+ title: "Hydrogen electrolyzer scheduling in microgrids",
+ doi: "10.7777/westlake.2026.014",
+ year: 2026,
+ venue: "Energy Systems Letters",
+ status: "published",
+ source: "doi-crosswalk",
+ authors: [
+ {
+ name: "Ada Kim",
+ orcid: "0000-0003-4444-7788",
+ affiliation: "Westlake Energy Systems",
+ },
+ { name: "Evan Stone", affiliation: "Westlake Energy Systems" },
+ ],
+ },
+ {
+ id: "pub-weak-but-reviewable",
+ title: "Sparse pathway graphs for reproducibility audits",
+ doi: "10.5555/northbridge.2026.081",
+ year: 2026,
+ venue: "Open Methods Review",
+ status: "published",
+ source: "publisher-feed",
+ authors: [
+ { name: "A. Kim", affiliation: "Northbridge Institute" },
+ { name: "Grace Lee", affiliation: "Northbridge Institute" },
+ ],
+ },
+ {
+ id: "pub-duplicate-doi",
+ title: "Grid-scale battery dispatch under volatile demand",
+ doi: "10.7777/westlake.2024.009",
+ year: 2024,
+ venue: "Energy Systems Letters",
+ status: "published",
+ source: "publisher-feed",
+ authors: [
+ { name: "Ada Kim", affiliation: "Westlake Energy Systems" },
+ { name: "Priya Raman", affiliation: "Westlake Energy Systems" },
+ ],
+ },
+ {
+ id: "pub-retracted",
+ title: "Automated profile scoring from unreconciled citations",
+ doi: "10.9999/example.2022.100",
+ year: 2022,
+ venue: "Synthetic Research Metrics",
+ status: "retracted",
+ source: "citation-import",
+ authors: [
+ { name: "Ada Kim", affiliation: "Northbridge Institute" },
+ { name: "Omar Singh", affiliation: "Northbridge Institute" },
+ ],
+ },
+];
+
+module.exports = {
+ researchers,
+ existingClaims,
+ importedPublications,
+};
diff --git a/orcid-publication-claim-guard/test.js b/orcid-publication-claim-guard/test.js
new file mode 100644
index 00000000..e10f589d
--- /dev/null
+++ b/orcid-publication-claim-guard/test.js
@@ -0,0 +1,77 @@
+const assert = require("assert");
+const {
+ evaluatePublicationClaims,
+ normalizeDoi,
+ normalizeText,
+ scorePublicationClaim,
+} = require("./index");
+const {
+ existingClaims,
+ importedPublications,
+ researchers,
+} = require("./sample-data");
+
+const primary = researchers[0];
+
+assert.strictEqual(
+ normalizeDoi("https://doi.org/10.5555/NORTHBRIDGE.2025.042"),
+ "10.5555/northbridge.2025.042"
+);
+assert.strictEqual(normalizeText("Ada M. Kim"), "ada m kim");
+
+const exact = scorePublicationClaim(
+ primary,
+ importedPublications.find((item) => item.id === "pub-exact-orcid"),
+ existingClaims
+);
+assert.strictEqual(exact.decision, "auto_accept");
+assert.ok(exact.score >= 72);
+assert.ok(exact.evidence.some((entry) => entry.includes("exact ORCID")));
+
+const homonym = scorePublicationClaim(
+ primary,
+ importedPublications.find((item) => item.id === "pub-homonym-orcid-mismatch"),
+ existingClaims
+);
+assert.strictEqual(homonym.decision, "quarantine");
+assert.ok(homonym.blockers.some((entry) => entry.includes("different ORCID")));
+
+const weak = scorePublicationClaim(
+ primary,
+ importedPublications.find((item) => item.id === "pub-weak-but-reviewable"),
+ existingClaims
+);
+assert.strictEqual(weak.decision, "review");
+assert.ok(weak.score > 30);
+assert.ok(weak.actions.some((entry) => entry.includes("reviewer approval")));
+
+const duplicate = scorePublicationClaim(
+ primary,
+ importedPublications.find((item) => item.id === "pub-duplicate-doi"),
+ existingClaims
+);
+assert.strictEqual(duplicate.decision, "quarantine");
+assert.ok(duplicate.blockers.some((entry) => entry.includes("already claimed")));
+
+const retracted = scorePublicationClaim(
+ primary,
+ importedPublications.find((item) => item.id === "pub-retracted"),
+ existingClaims
+);
+assert.strictEqual(retracted.decision, "quarantine");
+assert.ok(retracted.actions.some((entry) => entry.includes("profile metrics")));
+
+const report = evaluatePublicationClaims({
+ researcher: primary,
+ publications: importedPublications,
+ existingClaims,
+});
+assert.deepStrictEqual(report.summary, {
+ total: 5,
+ auto_accept: 1,
+ review: 1,
+ quarantine: 3,
+});
+assert.strictEqual(report.decisions[0].auditDigest.length, 64);
+
+console.log("orcid publication claim guard tests passed");