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 ` + + + + ORCID Publication Claim Guard + ${escapeXml(report.researcher.displayName)} - ${escapeXml(report.researcher.orcid)} + Accepted ${report.summary.auto_accept} / Review ${report.summary.review} / Quarantine ${report.summary.quarantine} + ${rows} +`; +} + +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 @@ + + + + + ORCID Publication Claim Guard + Ada Kim - 0000-0002-1825-0097 + Accepted 1 / Review 1 / Quarantine 3 + + + + + pub-exact-orcid - auto_accept + Reproducible notebooks for kinase pathway triage + score 86 + + + + + pub-homonym-orcid-mismatch - quarantine + Hydrogen electrolyzer scheduling in microgrids + score 14 + + + + + pub-weak-but-reviewable - review + Sparse pathway graphs for reproducibility audits + score 59 + + + + + pub-duplicate-doi - quarantine + Grid-scale battery dispatch under volatile demand + score 32 + + + + + pub-retracted - quarantine + Automated profile scoring from unreconciled citations + score 59 + + \ 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");