From 1afc0a87d5499718a26c78ad0be5626f5a5b77ce Mon Sep 17 00:00:00 2001 From: HunterCML <5335527+HunterCML@users.noreply.github.com> Date: Fri, 22 May 2026 13:45:54 -0500 Subject: [PATCH] Add reviewer expertise credential guard --- reviewer-expertise-credential-guard/README.md | 18 ++ .../acceptance-notes.md | 19 ++ reviewer-expertise-credential-guard/demo.js | 100 ++++++ reviewer-expertise-credential-guard/demo.mp4 | Bin 0 -> 40669 bytes reviewer-expertise-credential-guard/demo.svg | 27 ++ reviewer-expertise-credential-guard/index.js | 290 ++++++++++++++++++ .../reviewer-expertise-credential-report.json | 96 ++++++ .../reviewer-expertise-credential-report.md | 21 ++ .../requirements-map.md | 15 + reviewer-expertise-credential-guard/test.js | 105 +++++++ 10 files changed, 691 insertions(+) create mode 100644 reviewer-expertise-credential-guard/README.md create mode 100644 reviewer-expertise-credential-guard/acceptance-notes.md create mode 100644 reviewer-expertise-credential-guard/demo.js create mode 100644 reviewer-expertise-credential-guard/demo.mp4 create mode 100644 reviewer-expertise-credential-guard/demo.svg create mode 100644 reviewer-expertise-credential-guard/index.js create mode 100644 reviewer-expertise-credential-guard/reports/reviewer-expertise-credential-report.json create mode 100644 reviewer-expertise-credential-guard/reports/reviewer-expertise-credential-report.md create mode 100644 reviewer-expertise-credential-guard/requirements-map.md create mode 100644 reviewer-expertise-credential-guard/test.js diff --git a/reviewer-expertise-credential-guard/README.md b/reviewer-expertise-credential-guard/README.md new file mode 100644 index 00000000..79c2d09a --- /dev/null +++ b/reviewer-expertise-credential-guard/README.md @@ -0,0 +1,18 @@ +# Reviewer Expertise Credential Guard + +This module adds a focused community reputation guard for trusted reviewer expertise credentials. + +It evaluates whether a reviewer should receive weighted assignment credit or trusted-reviewer badge eligibility by checking declared expertise, evidence-backed domains, method credentials, evidence freshness, review history, conflicts of interest, and anonymous-review redaction. + +## Run + +```sh +node reviewer-expertise-credential-guard/test.js +node reviewer-expertise-credential-guard/demo.js +``` + +The demo writes JSON and Markdown reviewer artifacts to `reviewer-expertise-credential-guard/reports/`. + +## Review Surface + +The implementation is dependency-free, uses synthetic data only, and does not call external APIs or read credentials. diff --git a/reviewer-expertise-credential-guard/acceptance-notes.md b/reviewer-expertise-credential-guard/acceptance-notes.md new file mode 100644 index 00000000..aab68e5a --- /dev/null +++ b/reviewer-expertise-credential-guard/acceptance-notes.md @@ -0,0 +1,19 @@ +# Acceptance Notes + +## Validation + +- `node reviewer-expertise-credential-guard/test.js` +- `node reviewer-expertise-credential-guard/demo.js` +- `node --check reviewer-expertise-credential-guard/index.js` +- `node --check reviewer-expertise-credential-guard/test.js` +- `node --check reviewer-expertise-credential-guard/demo.js` +- `ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 reviewer-expertise-credential-guard/demo.mp4` + +## Acceptance Coverage + +- Current domain and method evidence can make a reviewer trusted-reviewer eligible. +- Missing domain evidence lowers review weight and requires steward review. +- Conflicts block weighted assignment credit. +- Stale credentials require refresh before trusted badge elevation. +- Anonymous review mode redacts display name and ORCID from public profiles. +- The output audit digest is deterministic for reviewer replay. diff --git a/reviewer-expertise-credential-guard/demo.js b/reviewer-expertise-credential-guard/demo.js new file mode 100644 index 00000000..d07fefe6 --- /dev/null +++ b/reviewer-expertise-credential-guard/demo.js @@ -0,0 +1,100 @@ +const fs = require("fs"); +const path = require("path"); +const { evaluateReviewerExpertiseCredentials } = require("./index"); + +const outputDir = path.join(__dirname, "reports"); +fs.mkdirSync(outputDir, { recursive: true }); + +const packet = { + now: "2026-06-01T12:00:00Z", + maxEvidenceAgeDays: 730, + minimumAcceptedReviews: 3, + reviewers: [ + { + id: "reviewer-ada", + displayName: "Ada Reviewer", + orcid: "0000-0002-1825-0097", + institution: "open-science-lab", + declaredDomains: ["proteomics", "machine-learning"], + declaredMethods: ["bayesian-modeling", "mass-spectrometry"], + acceptedReviews: 16, + evidence: [ + { type: "domain", domains: ["proteomics"], issuedAt: "2026-01-15T00:00:00Z", sourceId: "orcid-work-proteomics" }, + { type: "domain", domains: ["machine-learning"], issuedAt: "2026-02-10T00:00:00Z", sourceId: "grant-ml" }, + { type: "method", methods: ["bayesian-modeling"], issuedAt: "2026-03-01T00:00:00Z", sourceId: "review-method" }, + ], + conflicts: [], + }, + { + id: "reviewer-byron", + displayName: "Byron Reviewer", + institution: "northbridge-university", + declaredDomains: ["materials-science"], + declaredMethods: ["density-functional-theory"], + acceptedReviews: 2, + evidence: [{ type: "domain", domains: ["materials-science"], issuedAt: "2023-01-10T00:00:00Z", sourceId: "publication-old" }], + conflicts: [{ authorId: "author-lin", type: "recent-coauthor" }], + }, + ], + assignments: [ + { + id: "assign-protein", + manuscriptId: "ms-protein-forecast", + projectId: "project-protein", + reviewerId: "reviewer-ada", + requiredDomains: ["proteomics", "machine-learning"], + requiredMethods: ["bayesian-modeling"], + anonymousMode: true, + authorIds: ["author-lin"], + institutions: ["northbridge-university"], + }, + { + id: "assign-materials", + manuscriptId: "ms-materials-benchmark", + projectId: "project-materials", + reviewerId: "reviewer-byron", + requiredDomains: ["materials-science"], + requiredMethods: ["electron-microscopy"], + anonymousMode: false, + authorIds: ["author-lin"], + institutions: ["northbridge-university"], + }, + ], +}; + +const report = evaluateReviewerExpertiseCredentials(packet); +const jsonPath = path.join(outputDir, "reviewer-expertise-credential-report.json"); +const markdownPath = path.join(outputDir, "reviewer-expertise-credential-report.md"); + +fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2)); +fs.writeFileSync( + markdownPath, + [ + "# Reviewer Expertise Credential Guard Demo", + "", + `Decision: ${report.decision}`, + `Audit digest: ${report.auditDigest}`, + "", + "## Assignment Decisions", + "", + ...report.assignments.map( + (assignment) => + `- ${assignment.assignmentId}: ${assignment.decision}; badge ${assignment.badge}; weight ${assignment.reviewWeight}`, + ), + "", + "## Findings", + "", + ...report.assignments.flatMap((assignment) => + assignment.findings.map((finding) => `- ${assignment.assignmentId}: ${finding.severity} ${finding.code} - ${finding.message}`), + ), + "", + "## Public Profiles", + "", + ...report.assignments.map((assignment) => `- ${assignment.assignmentId}: ${JSON.stringify(assignment.publicProfile)}`), + "", + ].join("\n"), +); + +console.log(`Wrote ${jsonPath}`); +console.log(`Wrote ${markdownPath}`); +console.log(`${report.decision}: ${report.counts.findings} finding(s), ${report.auditDigest}`); diff --git a/reviewer-expertise-credential-guard/demo.mp4 b/reviewer-expertise-credential-guard/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..60acefc57bc115acacdbcf01ee5004cc5a94ad38 GIT binary patch literal 40669 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

UU?9l*K}YZ5Z^OQD`fucT}(Dod5yv0o!3LuN{7 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 + Reviewer Expertise Credential Guard Demo + A community reputation demo showing trusted-reviewer badge decisions from domain, method, freshness, conflict, and anonymity checks. + + + SCIBASE bounty demo artifact + Reviewer Expertise Credential Guard + Issue #15: community reputation and trusted reviewer badges + + + CREDENTIAL FLOW + 1. Match declared expertise to manuscript needs + 2. Verify domain and method evidence + 3. Detect stale claims and conflicts + 4. Redact anonymous public profile data + 5. Emit badge and review-weight decisions + $ node reviewer-expertise-credential-guard/test.js + tests passed: evidence, conflicts, stale claims, + anonymous redaction, deterministic digest + Reviewer artifacts + reports/reviewer-expertise-credential-report.json + reports/reviewer-expertise-credential-report.md + + + + Committed video demo + focused tests + acceptance notes + diff --git a/reviewer-expertise-credential-guard/index.js b/reviewer-expertise-credential-guard/index.js new file mode 100644 index 00000000..b3022a82 --- /dev/null +++ b/reviewer-expertise-credential-guard/index.js @@ -0,0 +1,290 @@ +const crypto = require("crypto"); + +function asArray(value) { + if (!value) return []; + return Array.isArray(value) ? value : [value]; +} + +function normalize(value) { + return String(value || "").trim().toLowerCase(); +} + +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return crypto.createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function parseDate(value) { + const time = Date.parse(value || ""); + return Number.isNaN(time) ? 0 : time; +} + +function unique(values) { + return [...new Set(asArray(values).map(normalize).filter(Boolean))]; +} + +function addFinding(findings, severity, code, message, remediation) { + findings.push({ severity, code, message, remediation }); +} + +function daysBetween(start, end) { + if (!start || !end) return Infinity; + return Math.floor(Math.abs(end - start) / 86400000); +} + +function evidenceMatches(evidence, kind, value) { + const target = normalize(value); + return asArray(evidence).filter((item) => { + if (kind && normalize(item.type) !== normalize(kind)) return false; + const domains = unique(item.domains || item.domain); + const methods = unique(item.methods || item.method); + const topics = unique(item.topics || item.topic); + return domains.includes(target) || methods.includes(target) || topics.includes(target); + }); +} + +function hasConflict(reviewer, assignment) { + const projectId = normalize(assignment.projectId || assignment.manuscriptId); + const authorIds = unique(assignment.authorIds); + const institutions = unique(assignment.institutions); + const funders = unique(assignment.funders); + const reviewerInstitution = normalize(reviewer.institution); + + for (const conflict of asArray(reviewer.conflicts)) { + const conflictProject = normalize(conflict.projectId || conflict.manuscriptId); + if (conflictProject && conflictProject === projectId) return conflict.type || "project"; + if (authorIds.includes(normalize(conflict.authorId))) return conflict.type || "author"; + if (funders.includes(normalize(conflict.funder))) return conflict.type || "funder"; + } + + if (reviewerInstitution && institutions.includes(reviewerInstitution)) { + return "shared-institution"; + } + + return ""; +} + +function newestEvidenceAgeDays(matches, now) { + if (matches.length === 0) return Infinity; + const newest = matches + .map((item) => parseDate(item.issuedAt || item.updatedAt || item.createdAt)) + .filter(Boolean) + .sort((a, b) => b - a)[0]; + return daysBetween(newest, now); +} + +function publicReviewerProfile(reviewer, assignment, score, badge) { + const expertiseTags = unique([ + ...asArray(reviewer.declaredDomains), + ...asArray(reviewer.declaredMethods), + ...asArray(reviewer.methods), + ]).slice(0, 8); + + if (assignment.anonymousMode) { + return { + reviewerHash: digest({ reviewerId: reviewer.id, manuscriptId: assignment.manuscriptId }).slice(0, 16), + expertiseTags, + expertiseScore: score, + badge, + identityRedacted: true, + }; + } + + return { + reviewerId: reviewer.id, + displayName: reviewer.displayName || reviewer.id, + orcid: reviewer.orcid, + expertiseTags, + expertiseScore: score, + badge, + identityRedacted: false, + }; +} + +function evaluateAssignment(assignment, reviewersById, context) { + const reviewer = reviewersById[assignment.reviewerId] || {}; + const findings = []; + const requiredDomains = unique(assignment.requiredDomains); + const requiredMethods = unique(assignment.requiredMethods); + const declaredDomains = unique(reviewer.declaredDomains); + const declaredMethods = unique(reviewer.declaredMethods || reviewer.methods); + const evidence = asArray(reviewer.evidence); + const maxEvidenceAgeDays = Number(assignment.maxEvidenceAgeDays || context.maxEvidenceAgeDays || 730); + let score = 100; + + if (!assignment.manuscriptId || !assignment.reviewerId) { + addFinding( + findings, + "blocker", + "ASSIGNMENT_CONTEXT_MISSING", + "Reviewer assignment must include a manuscript id and reviewer id.", + "Attach stable assignment context before weighting a review or awarding expertise reputation.", + ); + score -= 40; + } + + if (!reviewer.id) { + addFinding( + findings, + "blocker", + "REVIEWER_PROFILE_MISSING", + "The assignment references a reviewer profile that is not present in the packet.", + "Load the reviewer profile and expertise evidence before assignment.", + ); + score -= 40; + } + + const conflictType = hasConflict(reviewer, assignment); + if (conflictType) { + addFinding( + findings, + "blocker", + "REVIEWER_CONFLICT_DETECTED", + `Reviewer has a ${conflictType} conflict with this manuscript.`, + "Block weighted review credit until a steward resolves or reassigns the review.", + ); + score -= 45; + } + + for (const domain of requiredDomains) { + const matches = evidenceMatches(evidence, "domain", domain); + if (!declaredDomains.includes(domain)) { + addFinding( + findings, + "warning", + "DOMAIN_NOT_DECLARED", + `Reviewer has not declared the required domain ${domain}.`, + "Ask the reviewer to update profile expertise or route to a better matched reviewer.", + ); + score -= 10; + } + if (matches.length === 0) { + addFinding( + findings, + "warning", + "DOMAIN_EVIDENCE_MISSING", + `Reviewer lacks evidence for required domain ${domain}.`, + "Attach publication, ORCID, grant, or verified review evidence before awarding a trusted reviewer badge.", + ); + score -= 16; + } else if (newestEvidenceAgeDays(matches, context.now) > maxEvidenceAgeDays) { + addFinding( + findings, + "warning", + "DOMAIN_EVIDENCE_STALE", + `Reviewer evidence for ${domain} is older than ${maxEvidenceAgeDays} days.`, + "Refresh the credential evidence before using it for weighted reputation.", + ); + score -= 8; + } + } + + for (const method of requiredMethods) { + const matches = evidenceMatches(evidence, "method", method); + if (!declaredMethods.includes(method) || matches.length === 0) { + addFinding( + findings, + "warning", + "METHOD_EVIDENCE_GAP", + `Reviewer does not have current evidence for required method ${method}.`, + "Lower review weight or request a second reviewer with method-specific credentials.", + ); + score -= 12; + } + } + + const acceptedReviews = Number(reviewer.acceptedReviews || 0); + if (acceptedReviews < Number(context.minimumAcceptedReviews || 3)) { + addFinding( + findings, + "info", + "REVIEW_HISTORY_LIGHT", + "Reviewer has a short accepted-review history for trusted badge elevation.", + "Use steward review before granting a persistent expertise badge.", + ); + score -= 5; + } + + const normalizedScore = Math.max(0, Math.min(100, score)); + const blockers = findings.filter((finding) => finding.severity === "blocker"); + const warnings = findings.filter((finding) => finding.severity === "warning"); + const badge = + blockers.length > 0 + ? "badge-blocked" + : warnings.length > 0 || normalizedScore < 85 + ? "steward-review" + : "trusted-reviewer-eligible"; + const decision = + blockers.length > 0 + ? "block-assignment" + : warnings.length > 0 + ? "assign-with-lower-weight" + : "trusted-reviewer-ready"; + const weight = blockers.length > 0 ? 0 : Number((normalizedScore / 100).toFixed(2)); + + const result = { + assignmentId: assignment.id || `${assignment.manuscriptId}:${assignment.reviewerId}`, + manuscriptId: assignment.manuscriptId, + reviewerId: reviewer.id || assignment.reviewerId, + decision, + badge, + expertiseScore: normalizedScore, + reviewWeight: weight, + requiredDomains, + requiredMethods, + findings, + publicProfile: publicReviewerProfile(reviewer, assignment, normalizedScore, badge), + }; + + return { + ...result, + assignmentDigest: digest(result), + }; +} + +function evaluateReviewerExpertiseCredentials(packet = {}) { + const reviewersById = asArray(packet.reviewers).reduce((acc, reviewer) => { + if (reviewer && reviewer.id) acc[reviewer.id] = reviewer; + return acc; + }, {}); + const context = { + now: parseDate(packet.now || new Date().toISOString()), + maxEvidenceAgeDays: packet.maxEvidenceAgeDays || 730, + minimumAcceptedReviews: packet.minimumAcceptedReviews || 3, + }; + const assignments = asArray(packet.assignments).map((assignment) => evaluateAssignment(assignment, reviewersById, context)); + const counts = assignments.reduce( + (acc, assignment) => { + acc[assignment.decision] = (acc[assignment.decision] || 0) + 1; + acc.findings += assignment.findings.length; + return acc; + }, + { "trusted-reviewer-ready": 0, "assign-with-lower-weight": 0, "block-assignment": 0, findings: 0 }, + ); + const report = { + generatedAt: packet.now || new Date().toISOString(), + decision: counts["block-assignment"] > 0 ? "steward-intervention-required" : counts["assign-with-lower-weight"] > 0 ? "credential-review-needed" : "expertise-routing-ready", + counts, + assignments, + }; + + return { + ...report, + auditDigest: digest(report), + }; +} + +module.exports = { + evaluateReviewerExpertiseCredentials, + stableStringify, +}; diff --git a/reviewer-expertise-credential-guard/reports/reviewer-expertise-credential-report.json b/reviewer-expertise-credential-guard/reports/reviewer-expertise-credential-report.json new file mode 100644 index 00000000..c8190a6a --- /dev/null +++ b/reviewer-expertise-credential-guard/reports/reviewer-expertise-credential-report.json @@ -0,0 +1,96 @@ +{ + "generatedAt": "2026-06-01T12:00:00Z", + "decision": "steward-intervention-required", + "counts": { + "trusted-reviewer-ready": 1, + "assign-with-lower-weight": 0, + "block-assignment": 1, + "findings": 4 + }, + "assignments": [ + { + "assignmentId": "assign-protein", + "manuscriptId": "ms-protein-forecast", + "reviewerId": "reviewer-ada", + "decision": "trusted-reviewer-ready", + "badge": "trusted-reviewer-eligible", + "expertiseScore": 100, + "reviewWeight": 1, + "requiredDomains": [ + "proteomics", + "machine-learning" + ], + "requiredMethods": [ + "bayesian-modeling" + ], + "findings": [], + "publicProfile": { + "reviewerHash": "9f670a60af1c5a0d", + "expertiseTags": [ + "proteomics", + "machine-learning", + "bayesian-modeling", + "mass-spectrometry" + ], + "expertiseScore": 100, + "badge": "trusted-reviewer-eligible", + "identityRedacted": true + }, + "assignmentDigest": "b24195e29509d3f274f45b7eed4c20001a6c6327388b2ecb23b051032a635bb4" + }, + { + "assignmentId": "assign-materials", + "manuscriptId": "ms-materials-benchmark", + "reviewerId": "reviewer-byron", + "decision": "block-assignment", + "badge": "badge-blocked", + "expertiseScore": 30, + "reviewWeight": 0, + "requiredDomains": [ + "materials-science" + ], + "requiredMethods": [ + "electron-microscopy" + ], + "findings": [ + { + "severity": "blocker", + "code": "REVIEWER_CONFLICT_DETECTED", + "message": "Reviewer has a recent-coauthor conflict with this manuscript.", + "remediation": "Block weighted review credit until a steward resolves or reassigns the review." + }, + { + "severity": "warning", + "code": "DOMAIN_EVIDENCE_STALE", + "message": "Reviewer evidence for materials-science is older than 730 days.", + "remediation": "Refresh the credential evidence before using it for weighted reputation." + }, + { + "severity": "warning", + "code": "METHOD_EVIDENCE_GAP", + "message": "Reviewer does not have current evidence for required method electron-microscopy.", + "remediation": "Lower review weight or request a second reviewer with method-specific credentials." + }, + { + "severity": "info", + "code": "REVIEW_HISTORY_LIGHT", + "message": "Reviewer has a short accepted-review history for trusted badge elevation.", + "remediation": "Use steward review before granting a persistent expertise badge." + } + ], + "publicProfile": { + "reviewerId": "reviewer-byron", + "displayName": "Byron Reviewer", + "expertiseTags": [ + "materials-science", + "density-functional-theory" + ], + "expertiseScore": 30, + "badge": "badge-blocked", + "identityRedacted": false + }, + "assignmentDigest": "7c155f761071703d2621d08f53baa27406964bd9f07b3cf904a377bfa86f0bc6" + } + ], + "auditDigest": "3cb70effca2a0f1e197985e28369161e034e4a246dffcc30ae0ec116c747b820" +} \ No newline at end of file diff --git a/reviewer-expertise-credential-guard/reports/reviewer-expertise-credential-report.md b/reviewer-expertise-credential-guard/reports/reviewer-expertise-credential-report.md new file mode 100644 index 00000000..519c4ac8 --- /dev/null +++ b/reviewer-expertise-credential-guard/reports/reviewer-expertise-credential-report.md @@ -0,0 +1,21 @@ +# Reviewer Expertise Credential Guard Demo + +Decision: steward-intervention-required +Audit digest: 3cb70effca2a0f1e197985e28369161e034e4a246dffcc30ae0ec116c747b820 + +## Assignment Decisions + +- assign-protein: trusted-reviewer-ready; badge trusted-reviewer-eligible; weight 1 +- assign-materials: block-assignment; badge badge-blocked; weight 0 + +## Findings + +- assign-materials: blocker REVIEWER_CONFLICT_DETECTED - Reviewer has a recent-coauthor conflict with this manuscript. +- assign-materials: warning DOMAIN_EVIDENCE_STALE - Reviewer evidence for materials-science is older than 730 days. +- assign-materials: warning METHOD_EVIDENCE_GAP - Reviewer does not have current evidence for required method electron-microscopy. +- assign-materials: info REVIEW_HISTORY_LIGHT - Reviewer has a short accepted-review history for trusted badge elevation. + +## Public Profiles + +- assign-protein: {"reviewerHash":"9f670a60af1c5a0d","expertiseTags":["proteomics","machine-learning","bayesian-modeling","mass-spectrometry"],"expertiseScore":100,"badge":"trusted-reviewer-eligible","identityRedacted":true} +- assign-materials: {"reviewerId":"reviewer-byron","displayName":"Byron Reviewer","expertiseTags":["materials-science","density-functional-theory"],"expertiseScore":30,"badge":"badge-blocked","identityRedacted":false} diff --git a/reviewer-expertise-credential-guard/requirements-map.md b/reviewer-expertise-credential-guard/requirements-map.md new file mode 100644 index 00000000..d4544c38 --- /dev/null +++ b/reviewer-expertise-credential-guard/requirements-map.md @@ -0,0 +1,15 @@ +# Requirements Map + +Issue #15 asks for community and user reputation features, including reputation metrics, badges, user profiles, peer review, and trusted reviewer tiers. + +This submission focuses on a separate expertise credential lane: + +- Validates that reviewer domain claims are declared and backed by evidence. +- Checks method-specific evidence before assigning weighted review reputation. +- Detects stale credentials and routes them to steward refresh. +- Blocks weighted reviews when author, institution, funder, or project conflicts are present. +- Supports anonymous review by emitting redacted public reviewer profiles. +- Produces trusted-reviewer, steward-review, and badge-blocked outcomes. +- Emits deterministic assignment and report digests for reviewer replay. + +The scope is intentionally narrow so it does not overlap with generic reputation scoring, leaderboard eligibility, recusal, review civility, endorsement-ring, correction-impact, or badge-renewal submissions. diff --git a/reviewer-expertise-credential-guard/test.js b/reviewer-expertise-credential-guard/test.js new file mode 100644 index 00000000..1f9552bc --- /dev/null +++ b/reviewer-expertise-credential-guard/test.js @@ -0,0 +1,105 @@ +const assert = require("assert"); +const { evaluateReviewerExpertiseCredentials } = require("./index"); + +function basePacket(overrides = {}) { + return { + now: "2026-06-01T12:00:00Z", + maxEvidenceAgeDays: 730, + minimumAcceptedReviews: 3, + reviewers: [ + { + id: "reviewer-ada", + displayName: "Ada Reviewer", + orcid: "0000-0002-1825-0097", + institution: "open-science-lab", + declaredDomains: ["proteomics", "machine-learning"], + declaredMethods: ["bayesian-modeling", "mass-spectrometry"], + acceptedReviews: 14, + evidence: [ + { type: "domain", domains: ["proteomics"], issuedAt: "2026-01-15T00:00:00Z", sourceId: "orcid-work-1" }, + { type: "domain", domains: ["machine-learning"], issuedAt: "2026-02-10T00:00:00Z", sourceId: "grant-ml-7" }, + { type: "method", methods: ["bayesian-modeling"], issuedAt: "2026-03-01T00:00:00Z", sourceId: "review-method-4" }, + { type: "method", methods: ["mass-spectrometry"], issuedAt: "2026-03-05T00:00:00Z", sourceId: "publication-ms-2" }, + ], + conflicts: [], + }, + ], + assignments: [ + { + id: "assign-1", + manuscriptId: "ms-protein-forecast", + projectId: "project-protein", + reviewerId: "reviewer-ada", + requiredDomains: ["proteomics", "machine-learning"], + requiredMethods: ["bayesian-modeling"], + anonymousMode: true, + authorIds: ["author-lin"], + institutions: ["northbridge-university"], + }, + ], + ...overrides, + }; +} + +function testTrustedReviewerEligibleWithCurrentEvidence() { + const result = evaluateReviewerExpertiseCredentials(basePacket()); + + assert.equal(result.decision, "expertise-routing-ready"); + assert.equal(result.assignments[0].decision, "trusted-reviewer-ready"); + assert.equal(result.assignments[0].badge, "trusted-reviewer-eligible"); + assert.equal(result.assignments[0].reviewWeight, 1); +} + +function testMissingDomainEvidenceRequiresStewardReview() { + const packet = basePacket(); + packet.reviewers[0].evidence = packet.reviewers[0].evidence.filter((item) => !item.domains || !item.domains.includes("machine-learning")); + const result = evaluateReviewerExpertiseCredentials(packet); + + assert.equal(result.decision, "credential-review-needed"); + assert.equal(result.assignments[0].decision, "assign-with-lower-weight"); + assert.ok(result.assignments[0].findings.some((finding) => finding.code === "DOMAIN_EVIDENCE_MISSING")); +} + +function testConflictBlocksWeightedReview() { + const packet = basePacket(); + packet.reviewers[0].conflicts = [{ authorId: "author-lin", type: "recent-coauthor" }]; + const result = evaluateReviewerExpertiseCredentials(packet); + + assert.equal(result.decision, "steward-intervention-required"); + assert.equal(result.assignments[0].decision, "block-assignment"); + assert.equal(result.assignments[0].reviewWeight, 0); + assert.ok(result.assignments[0].findings.some((finding) => finding.code === "REVIEWER_CONFLICT_DETECTED")); +} + +function testStaleEvidenceRequiresRefresh() { + const packet = basePacket({ now: "2028-06-01T12:00:00Z" }); + const result = evaluateReviewerExpertiseCredentials(packet); + + assert.equal(result.decision, "credential-review-needed"); + assert.ok(result.assignments[0].findings.some((finding) => finding.code === "DOMAIN_EVIDENCE_STALE")); +} + +function testAnonymousProfileRedactsIdentity() { + const result = evaluateReviewerExpertiseCredentials(basePacket()); + const profile = result.assignments[0].publicProfile; + + assert.equal(profile.identityRedacted, true); + assert.ok(profile.reviewerHash); + assert.equal(profile.displayName, undefined); + assert.equal(profile.orcid, undefined); +} + +function testDeterministicDigest() { + const first = evaluateReviewerExpertiseCredentials(basePacket()); + const second = evaluateReviewerExpertiseCredentials(basePacket()); + assert.equal(first.auditDigest, second.auditDigest); +} + +testTrustedReviewerEligibleWithCurrentEvidence(); +testMissingDomainEvidenceRequiresStewardReview(); +testConflictBlocksWeightedReview(); +testStaleEvidenceRequiresRefresh(); +testAnonymousProfileRedactsIdentity(); +testDeterministicDigest(); + +console.log("reviewer-expertise-credential-guard tests passed");