diff --git a/peer-review-citation-coercion-guard/README.md b/peer-review-citation-coercion-guard/README.md new file mode 100644 index 00000000..76930a44 --- /dev/null +++ b/peer-review-citation-coercion-guard/README.md @@ -0,0 +1,23 @@ +# Peer Review Citation Coercion Guard + +This module adds a focused Community & User Reputation slice for SCIBASE issue #15. It evaluates structured peer reviews before they update reviewer reputation, badges, leaderboards, or project timelines when citation pressure is present. + +The guard detects unsupported reviewer self-citation demands, reciprocal citation pressure, journal or editor citation quotas, and clusters of off-topic mandatory citation requests. It also protects authors from reputation or badge penalties while a steward reviews the citation pressure. + +## Run + +```bash +npm test +npm run demo +npm run video +npm run check +``` + +## Outputs + +- `reports/citation-coercion-packet.json` +- `reports/citation-coercion-report.md` +- `reports/summary.svg` +- `reports/demo.mp4` + +All data is synthetic. The module does not use credentials, private users, live review systems, payment systems, or external APIs. diff --git a/peer-review-citation-coercion-guard/acceptance-notes.md b/peer-review-citation-coercion-guard/acceptance-notes.md new file mode 100644 index 00000000..95771521 --- /dev/null +++ b/peer-review-citation-coercion-guard/acceptance-notes.md @@ -0,0 +1,17 @@ +# Acceptance Notes + +The implemented slice is intentionally distinct from existing #15 submissions: + +- It is not a broad reputation ledger. +- It is not citation context-fit, citation venue integrity, or citation diversity work for the research-tools issue. +- It is not review civility, recusal or COI assignment, workload equity, rubric validation, accessibility, identity verification, CRediT role evidence, profile visibility, or evidence recertification. +- It focuses on reviewer-driven citation coercion before peer reviews affect reputation, badges, leaderboards, or project timelines. + +Validation targets: + +- unsupported mandatory reviewer self-citations trigger steward review +- reciprocal citation pressure is flagged without punishing authors +- journal citation quota pressure blocks reputation changes +- legitimate contextual citation suggestions still earn reviewer credit +- double-blind reviewer identity is redacted from outputs +- summary counts and audit digest are deterministic diff --git a/peer-review-citation-coercion-guard/demo.js b/peer-review-citation-coercion-guard/demo.js new file mode 100644 index 00000000..522fdcf2 --- /dev/null +++ b/peer-review-citation-coercion-guard/demo.js @@ -0,0 +1,85 @@ +const fs = require('fs'); +const path = require('path'); +const { + evaluateCitationCoercion, + buildSampleReviewPacket +} = require('./index'); + +const reportsDir = path.join(__dirname, 'reports'); +fs.mkdirSync(reportsDir, { recursive: true }); + +const packet = buildSampleReviewPacket(); +const result = evaluateCitationCoercion(packet); + +const packetPath = path.join(reportsDir, 'citation-coercion-packet.json'); +const reportPath = path.join(reportsDir, 'citation-coercion-report.md'); +const svgPath = path.join(reportsDir, 'summary.svg'); + +fs.writeFileSync(packetPath, `${JSON.stringify(result, null, 2)}\n`); + +const flaggedReviews = result.reviewDecisions + .filter((decision) => decision.decision === 'steward-review-required') + .map((decision) => `- ${decision.id}: ${decision.flags.join(', ')}`) + .join('\n'); + +const taskList = result.stewardTasks + .map((task) => `- ${task.id} (${task.priority}): ${task.requiredAction}`) + .join('\n'); + +const authorProtections = result.authorProtections + .map((protection) => `- ${protection.projectId}: ${protection.blockedUpdates.join(', ')}`) + .join('\n'); + +const markdown = `# Peer Review Citation Coercion Guard + +Packet: ${result.packetId} +Generated: ${result.generatedAt} + +## Summary + +- Total reviews evaluated: ${result.summary.totalReviews} +- Flagged reviews: ${result.summary.flaggedReviews} +- Held reviewer credit: ${result.summary.heldReviewerCredit} +- Protected projects: ${result.summary.protectedProjects} +- Recommended action: ${result.summary.recommendedAction} +- Audit digest: ${result.auditPacket.auditDigest} + +## Flagged Citation Pressure + +${flaggedReviews} + +## Steward Tasks + +${taskList} + +## Author Protections + +${authorProtections} + +## Privacy Notes + +Double-blind reviewer identifiers are replaced with anonymous labels before reports, tasks, and timeline events are emitted. The packet uses synthetic data only and does not contain credentials, private profile emails, live user data, external API calls, payment systems, or real review records. +`; + +fs.writeFileSync(reportPath, markdown); + +const svg = ` + + + Peer Review Citation Coercion Guard + Flagged reviews: ${result.summary.flaggedReviews} / ${result.summary.totalReviews} + Held reviewer credit: ${result.summary.heldReviewerCredit} + Protected projects: ${result.summary.protectedProjects} + Action: ${result.summary.recommendedAction} + Detects self-citation demands, reciprocal pressure, and journal quota coercion. + Blind reviewer labels are preserved without raw identity leakage. + ${result.auditPacket.auditDigest} + +`; + +fs.writeFileSync(svgPath, svg); + +console.log(`Wrote ${path.relative(__dirname, packetPath)}`); +console.log(`Wrote ${path.relative(__dirname, reportPath)}`); +console.log(`Wrote ${path.relative(__dirname, svgPath)}`); +console.log(`Recommended action: ${result.summary.recommendedAction}`); diff --git a/peer-review-citation-coercion-guard/index.js b/peer-review-citation-coercion-guard/index.js new file mode 100644 index 00000000..d0116272 --- /dev/null +++ b/peer-review-citation-coercion-guard/index.js @@ -0,0 +1,425 @@ +const crypto = require('crypto'); + +const BLOCKED_UPDATES = [ + 'reviewer-reputation', + 'trusted-reviewer-badge', + 'leaderboards', + 'project-timeline' +]; + +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 `sha256:${crypto.createHash('sha256').update(stableStringify(value)).digest('hex')}`; +} + +function isBlindReview(mode) { + return ['anonymous', 'blind', 'double-blind', 'fully-anonymous'].includes(mode); +} + +function reviewerLabel(review) { + if (isBlindReview(review.mode)) { + return review.anonymousLabel || 'anonymous-reviewer'; + } + + return `reviewer:${review.reviewerId}`; +} + +function unique(values) { + return [...new Set(values)]; +} + +function pressureText(citation) { + return `${citation.requestText || ''} ${citation.condition || ''}`.toLowerCase(); +} + +function hasReciprocalPressure(citation) { + const text = pressureText(citation); + return Boolean( + citation.reciprocalPromise || + text.includes('cite mine and i will cite yours') || + text.includes('reciprocal citation') || + text.includes('mutual citation') || + text.includes('citation exchange') + ); +} + +function hasQuotaPressure(citation) { + const text = pressureText(citation); + return Boolean( + citation.journalQuota || + citation.editorialQuota || + text.includes('journal impact factor') || + text.includes('citation quota') || + text.includes('minimum journal citations') || + text.includes('cite at least') + ); +} + +function isCoerciveRequirement(citation) { + const text = pressureText(citation); + return Boolean( + citation.requiredForAcceptance || + text.includes('must cite') || + text.includes('required for acceptance') || + text.includes('before i can recommend acceptance') || + text.includes('mandatory') + ); +} + +function evaluateCitation(review, citation) { + const flags = []; + const reasons = []; + const relevanceScore = citation.relevanceScore ?? 0; + + if (citation.requestedByReviewerAuthor && relevanceScore < 0.5 && isCoerciveRequirement(citation)) { + flags.push('unsupported-self-citation-demand'); + reasons.push('reviewer-authored citation is mandatory but weakly related to the manuscript'); + } + + if (hasReciprocalPressure(citation)) { + flags.push('reciprocal-citation-pressure'); + reasons.push('citation request offers or implies reciprocal citation benefit'); + } + + if (hasQuotaPressure(citation)) { + flags.push('journal-citation-quota-pressure'); + reasons.push('citation request appears tied to journal or editor citation quota pressure'); + } + + return { + citationId: citation.id, + title: citation.title, + relevanceScore, + requestedByReviewerAuthor: Boolean(citation.requestedByReviewerAuthor), + requestedByEditorOrVenue: Boolean(citation.requestedByEditorOrVenue || citation.journalQuota), + requiredForAcceptance: Boolean(isCoerciveRequirement(citation)), + flags: unique(flags), + reasons, + reviewMode: review.mode + }; +} + +function findReviewFlags(review, citationFindings) { + const flags = citationFindings.flatMap((finding) => finding.flags); + const lowRelevanceRequired = review.requestedCitations.filter( + (citation) => (citation.relevanceScore ?? 0) < 0.35 && isCoerciveRequirement(citation) + ); + + if (lowRelevanceRequired.length >= 2) { + flags.push('off-topic-citation-cluster'); + } + + return unique(flags); +} + +function highestSeverity(flags) { + if ( + flags.includes('unsupported-self-citation-demand') || + flags.includes('reciprocal-citation-pressure') || + flags.includes('journal-citation-quota-pressure') + ) { + return 'high'; + } + + if (flags.length > 0) { + return 'medium'; + } + + return 'none'; +} + +function evaluateReview(review) { + const citationFindings = review.requestedCitations.map((citation) => evaluateCitation(review, citation)); + const flags = findReviewFlags(review, citationFindings); + const flagged = flags.length > 0; + + return { + id: review.id, + projectId: review.projectId, + reviewer: reviewerLabel(review), + mode: review.mode, + submittedAt: review.submittedAt, + decision: flagged ? 'steward-review-required' : 'allow-reputation-update', + highestSeverity: highestSeverity(flags), + flags, + citationFindings, + requiredStewardAction: flagged + ? 'review-citation-demands-before-score-or-badge-update' + : 'none' + }; +} + +function buildReputationAction(review, decision) { + if (decision.decision === 'allow-reputation-update') { + return { + id: `apply-${review.id}`, + reviewId: review.id, + reviewer: decision.reviewer, + action: 'apply-reviewer-credit', + originalReviewerDelta: review.reviewerDelta, + effectiveReviewerDelta: review.reviewerDelta, + evidenceDigest: digest({ + reviewId: review.id, + decision: decision.decision, + flags: decision.flags + }) + }; + } + + return { + id: `hold-${review.id}`, + reviewId: review.id, + reviewer: decision.reviewer, + action: 'hold-reviewer-credit', + originalReviewerDelta: review.reviewerDelta, + effectiveReviewerDelta: 0, + blockedUpdates: BLOCKED_UPDATES, + evidenceDigest: digest({ + reviewId: review.id, + decision: decision.decision, + flags: decision.flags + }) + }; +} + +function buildStewardTask(review, decision) { + if (decision.decision === 'allow-reputation-update') { + return null; + } + + return { + id: `task-${review.id}`, + reviewId: review.id, + projectId: review.projectId, + reviewer: decision.reviewer, + priority: decision.highestSeverity === 'high' ? 'high' : 'normal', + requiredAction: 'remove-or-justify-coercive-citation-requests', + blockedUpdates: BLOCKED_UPDATES, + flags: decision.flags, + citationIds: decision.citationFindings + .filter((finding) => finding.flags.length > 0 || finding.requiredForAcceptance) + .map((finding) => finding.citationId) + }; +} + +function buildAuthorProtections(reviewDecisions) { + const protectedProjects = new Map(); + + for (const decision of reviewDecisions) { + if (decision.decision !== 'steward-review-required') { + continue; + } + + if (!protectedProjects.has(decision.projectId)) { + protectedProjects.set(decision.projectId, { + id: `protect-${decision.projectId}`, + projectId: decision.projectId, + action: 'block-author-penalty', + reasons: ['citation-pressure-under-review'], + blockedUpdates: [ + 'author-response-penalty', + 'project-quality-badge-decay', + 'timeline-negative-review-event' + ], + relatedReviewIds: [] + }); + } + + protectedProjects.get(decision.projectId).relatedReviewIds.push(decision.id); + } + + return [...protectedProjects.values()]; +} + +function buildAuditPacket(packet, reviewDecisions, reputationActions, stewardTasks, authorProtections) { + const events = [ + ...reviewDecisions.map((decision) => ({ + type: decision.decision === 'allow-reputation-update' ? 'citation-review-allowed' : 'citation-pressure-held', + reviewId: decision.id, + projectId: decision.projectId, + reviewer: decision.reviewer, + flags: decision.flags, + decision: decision.decision + })), + ...stewardTasks.map((task) => ({ + type: 'steward-task-created', + taskId: task.id, + reviewId: task.reviewId, + projectId: task.projectId, + priority: task.priority + })), + ...authorProtections.map((protection) => ({ + type: 'author-protection-applied', + projectId: protection.projectId, + relatedReviewIds: protection.relatedReviewIds + })) + ]; + + const auditDigest = digest({ + packetId: packet.packetId, + generatedAt: packet.asOf, + events + }); + + return { + packetId: packet.packetId, + generatedAt: packet.asOf, + events, + auditDigest + }; +} + +function evaluateCitationCoercion(packet) { + const reviewDecisions = packet.reviews.map(evaluateReview); + const reputationActions = packet.reviews.map((review, index) => + buildReputationAction(review, reviewDecisions[index]) + ); + const stewardTasks = packet.reviews + .map((review, index) => buildStewardTask(review, reviewDecisions[index])) + .filter(Boolean); + const authorProtections = buildAuthorProtections(reviewDecisions); + const auditPacket = buildAuditPacket(packet, reviewDecisions, reputationActions, stewardTasks, authorProtections); + const heldReviewerCredit = reputationActions + .filter((action) => action.action === 'hold-reviewer-credit') + .reduce((total, action) => total + action.originalReviewerDelta, 0); + + return { + packetId: packet.packetId, + generatedAt: packet.asOf, + reviewDecisions, + reputationActions, + stewardTasks, + authorProtections, + auditPacket, + summary: { + totalReviews: reviewDecisions.length, + flaggedReviews: reviewDecisions.filter((decision) => decision.decision === 'steward-review-required').length, + heldReviewerCredit, + protectedProjects: authorProtections.length, + recommendedAction: + stewardTasks.length > 0 + ? 'pause-coerced-citation-reputation-updates' + : 'allow-citation-related-reputation-updates' + } + }; +} + +function buildSampleReviewPacket() { + return { + packetId: 'citation-coercion-review-batch-2026-05-28', + asOf: '2026-05-28T12:30:00Z', + reviews: [ + { + id: 'review-r1-self-citation', + projectId: 'project-neuro-open', + reviewerId: 'orcid:0000-0001-reviewer-alpha', + mode: 'public', + submittedAt: '2026-05-28T09:00:00Z', + reviewerDelta: 10, + requestedCitations: [ + { + id: 'cite-alpha-2024', + title: 'Alpha Lab Editorial Notes On Cognitive Maps', + relevanceScore: 0.22, + requestedByReviewerAuthor: true, + requiredForAcceptance: true, + requestText: 'You must cite this before I can recommend acceptance.' + }, + { + id: 'cite-alpha-2023', + title: 'Unrelated Rodent Navigation Commentary', + relevanceScore: 0.18, + requestedByReviewerAuthor: true, + requiredForAcceptance: true, + requestText: 'This reviewer-authored citation is mandatory.' + }, + { + id: 'cite-domain-2019', + title: 'Domain Background Review', + relevanceScore: 0.71, + requestedByReviewerAuthor: false, + requiredForAcceptance: false, + requestText: 'Consider as optional context.' + } + ] + }, + { + id: 'review-r2-reciprocal-pressure', + projectId: 'project-neuro-open', + reviewerId: 'orcid:0000-0002-private-pressure', + anonymousLabel: 'anonymous-reviewer-42', + mode: 'double-blind', + submittedAt: '2026-05-28T09:30:00Z', + reviewerDelta: 7, + requestedCitations: [ + { + id: 'cite-mutual-2025', + title: 'Mutual Citation Network Preprint', + relevanceScore: 0.55, + requestedByReviewerAuthor: true, + reciprocalPromise: true, + requiredForAcceptance: true, + requestText: 'Cite mine and I will cite yours in our next review.' + } + ] + }, + { + id: 'review-r3-quota-pressure', + projectId: 'project-protein-open', + reviewerId: 'orcid:0000-0003-editorial-pressure', + mode: 'semi-private', + submittedAt: '2026-05-28T10:00:00Z', + reviewerDelta: 6, + requestedCitations: [ + { + id: 'cite-journal-quota-2026', + title: 'Recent Articles From The Target Journal', + relevanceScore: 0.42, + requestedByEditorOrVenue: true, + journalQuota: true, + requiredForAcceptance: true, + requestText: 'Cite at least three recent papers from this journal to satisfy the journal impact factor target.' + } + ] + }, + { + id: 'review-r4-legitimate-context', + projectId: 'project-microbiome-open', + reviewerId: 'orcid:0000-0004-methods-reviewer', + mode: 'public', + submittedAt: '2026-05-28T10:30:00Z', + reviewerDelta: 8, + requestedCitations: [ + { + id: 'cite-methods-2021', + title: 'Benchmark Dataset Normalization Methods', + relevanceScore: 0.91, + requestedByReviewerAuthor: false, + requiredForAcceptance: false, + requestText: 'This optional context may help readers compare normalization methods.' + } + ] + } + ] + }; +} + +module.exports = { + evaluateCitationCoercion, + buildSampleReviewPacket, + digest +}; diff --git a/peer-review-citation-coercion-guard/make-demo-video.py b/peer-review-citation-coercion-guard/make-demo-video.py new file mode 100644 index 00000000..6286b31b --- /dev/null +++ b/peer-review-citation-coercion-guard/make-demo-video.py @@ -0,0 +1,71 @@ +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +ROOT = Path(__file__).resolve().parent +REPORTS = ROOT / "reports" +REPORTS.mkdir(exist_ok=True) +OUTPUT = REPORTS / "demo.mp4" + + +def load_font(size): + for candidate in [ + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/segoeui.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + ]: + path = Path(candidate) + if path.exists(): + return ImageFont.truetype(str(path), size) + return ImageFont.load_default() + + +def frame(progress): + image = Image.new("RGB", (1280, 720), "#13221c") + draw = ImageDraw.Draw(image) + draw.rounded_rectangle((60, 70, 1220, 650), radius=18, fill="#1d342c", outline="#8ee6b5", width=4) + + title = load_font(44) + heading = load_font(30) + body = load_font(24) + small = load_font(18) + + draw.text((100, 125), "Peer Review Citation Coercion Guard", fill="white", font=title) + lines = [ + "Detects mandatory self-citation requests", + "Flags reciprocal citation pressure", + "Blocks journal quota driven citation demands", + "Holds reviewer credit until steward review", + "Protects authors from reputation penalties", + "Redacts double-blind reviewer identity" + ] + + visible = min(len(lines), 1 + int(progress * len(lines))) + for index, line in enumerate(lines[:visible]): + y = 205 + index * 54 + draw.text((100, y), line, fill="#d9fff0", font=heading if index < 3 else body) + + bar_width = int(980 * progress) + draw.rounded_rectangle((100, 590, 1080, 615), radius=10, fill="#294d40") + draw.rounded_rectangle((100, 590, 100 + bar_width, 615), radius=10, fill="#8ee6b5") + draw.text((100, 635), "SCIBASE issue #15 community reputation slice - synthetic demo", fill="#a8dcc8", font=small) + return image + + +def main(): + try: + import imageio.v3 as iio + except Exception as exc: # pragma: no cover - helper path for local artifact generation + raise SystemExit( + "imageio and imageio-ffmpeg are required to regenerate reports/demo.mp4. " + "The committed demo.mp4 is already generated for review." + ) from exc + + frames = [frame(index / 59) for index in range(60)] + iio.imwrite(OUTPUT, frames, fps=15, codec="libx264", quality=8, macro_block_size=16) + print(f"Wrote {OUTPUT.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/peer-review-citation-coercion-guard/package.json b/peer-review-citation-coercion-guard/package.json new file mode 100644 index 00000000..f17e7f55 --- /dev/null +++ b/peer-review-citation-coercion-guard/package.json @@ -0,0 +1,12 @@ +{ + "name": "peer-review-citation-coercion-guard", + "version": "1.0.0", + "private": true, + "description": "Dependency-free peer-review citation coercion guard for SCIBASE issue #15.", + "scripts": { + "test": "node test.js", + "demo": "node demo.js", + "video": "python make-demo-video.py", + "check": "node --check index.js && node --check test.js && node --check demo.js" + } +} diff --git a/peer-review-citation-coercion-guard/reports/citation-coercion-packet.json b/peer-review-citation-coercion-guard/reports/citation-coercion-packet.json new file mode 100644 index 00000000..671eaa4f --- /dev/null +++ b/peer-review-citation-coercion-guard/reports/citation-coercion-packet.json @@ -0,0 +1,391 @@ +{ + "packetId": "citation-coercion-review-batch-2026-05-28", + "generatedAt": "2026-05-28T12:30:00Z", + "reviewDecisions": [ + { + "id": "review-r1-self-citation", + "projectId": "project-neuro-open", + "reviewer": "reviewer:orcid:0000-0001-reviewer-alpha", + "mode": "public", + "submittedAt": "2026-05-28T09:00:00Z", + "decision": "steward-review-required", + "highestSeverity": "high", + "flags": [ + "unsupported-self-citation-demand", + "off-topic-citation-cluster" + ], + "citationFindings": [ + { + "citationId": "cite-alpha-2024", + "title": "Alpha Lab Editorial Notes On Cognitive Maps", + "relevanceScore": 0.22, + "requestedByReviewerAuthor": true, + "requestedByEditorOrVenue": false, + "requiredForAcceptance": true, + "flags": [ + "unsupported-self-citation-demand" + ], + "reasons": [ + "reviewer-authored citation is mandatory but weakly related to the manuscript" + ], + "reviewMode": "public" + }, + { + "citationId": "cite-alpha-2023", + "title": "Unrelated Rodent Navigation Commentary", + "relevanceScore": 0.18, + "requestedByReviewerAuthor": true, + "requestedByEditorOrVenue": false, + "requiredForAcceptance": true, + "flags": [ + "unsupported-self-citation-demand" + ], + "reasons": [ + "reviewer-authored citation is mandatory but weakly related to the manuscript" + ], + "reviewMode": "public" + }, + { + "citationId": "cite-domain-2019", + "title": "Domain Background Review", + "relevanceScore": 0.71, + "requestedByReviewerAuthor": false, + "requestedByEditorOrVenue": false, + "requiredForAcceptance": false, + "flags": [], + "reasons": [], + "reviewMode": "public" + } + ], + "requiredStewardAction": "review-citation-demands-before-score-or-badge-update" + }, + { + "id": "review-r2-reciprocal-pressure", + "projectId": "project-neuro-open", + "reviewer": "anonymous-reviewer-42", + "mode": "double-blind", + "submittedAt": "2026-05-28T09:30:00Z", + "decision": "steward-review-required", + "highestSeverity": "high", + "flags": [ + "reciprocal-citation-pressure" + ], + "citationFindings": [ + { + "citationId": "cite-mutual-2025", + "title": "Mutual Citation Network Preprint", + "relevanceScore": 0.55, + "requestedByReviewerAuthor": true, + "requestedByEditorOrVenue": false, + "requiredForAcceptance": true, + "flags": [ + "reciprocal-citation-pressure" + ], + "reasons": [ + "citation request offers or implies reciprocal citation benefit" + ], + "reviewMode": "double-blind" + } + ], + "requiredStewardAction": "review-citation-demands-before-score-or-badge-update" + }, + { + "id": "review-r3-quota-pressure", + "projectId": "project-protein-open", + "reviewer": "reviewer:orcid:0000-0003-editorial-pressure", + "mode": "semi-private", + "submittedAt": "2026-05-28T10:00:00Z", + "decision": "steward-review-required", + "highestSeverity": "high", + "flags": [ + "journal-citation-quota-pressure" + ], + "citationFindings": [ + { + "citationId": "cite-journal-quota-2026", + "title": "Recent Articles From The Target Journal", + "relevanceScore": 0.42, + "requestedByReviewerAuthor": false, + "requestedByEditorOrVenue": true, + "requiredForAcceptance": true, + "flags": [ + "journal-citation-quota-pressure" + ], + "reasons": [ + "citation request appears tied to journal or editor citation quota pressure" + ], + "reviewMode": "semi-private" + } + ], + "requiredStewardAction": "review-citation-demands-before-score-or-badge-update" + }, + { + "id": "review-r4-legitimate-context", + "projectId": "project-microbiome-open", + "reviewer": "reviewer:orcid:0000-0004-methods-reviewer", + "mode": "public", + "submittedAt": "2026-05-28T10:30:00Z", + "decision": "allow-reputation-update", + "highestSeverity": "none", + "flags": [], + "citationFindings": [ + { + "citationId": "cite-methods-2021", + "title": "Benchmark Dataset Normalization Methods", + "relevanceScore": 0.91, + "requestedByReviewerAuthor": false, + "requestedByEditorOrVenue": false, + "requiredForAcceptance": false, + "flags": [], + "reasons": [], + "reviewMode": "public" + } + ], + "requiredStewardAction": "none" + } + ], + "reputationActions": [ + { + "id": "hold-review-r1-self-citation", + "reviewId": "review-r1-self-citation", + "reviewer": "reviewer:orcid:0000-0001-reviewer-alpha", + "action": "hold-reviewer-credit", + "originalReviewerDelta": 10, + "effectiveReviewerDelta": 0, + "blockedUpdates": [ + "reviewer-reputation", + "trusted-reviewer-badge", + "leaderboards", + "project-timeline" + ], + "evidenceDigest": "sha256:e47ae057aab045d09cd71b67fae4e71de856f625be2db42d63520c006e978017" + }, + { + "id": "hold-review-r2-reciprocal-pressure", + "reviewId": "review-r2-reciprocal-pressure", + "reviewer": "anonymous-reviewer-42", + "action": "hold-reviewer-credit", + "originalReviewerDelta": 7, + "effectiveReviewerDelta": 0, + "blockedUpdates": [ + "reviewer-reputation", + "trusted-reviewer-badge", + "leaderboards", + "project-timeline" + ], + "evidenceDigest": "sha256:65f2d44483fc7d53bfbe65a59c7c36f485d33c3d291c7a4047f4b739e349fc94" + }, + { + "id": "hold-review-r3-quota-pressure", + "reviewId": "review-r3-quota-pressure", + "reviewer": "reviewer:orcid:0000-0003-editorial-pressure", + "action": "hold-reviewer-credit", + "originalReviewerDelta": 6, + "effectiveReviewerDelta": 0, + "blockedUpdates": [ + "reviewer-reputation", + "trusted-reviewer-badge", + "leaderboards", + "project-timeline" + ], + "evidenceDigest": "sha256:4815f9ae8aa71fd9fd867004fdd7546fbb886c875bc54fbd916b2f659c039f74" + }, + { + "id": "apply-review-r4-legitimate-context", + "reviewId": "review-r4-legitimate-context", + "reviewer": "reviewer:orcid:0000-0004-methods-reviewer", + "action": "apply-reviewer-credit", + "originalReviewerDelta": 8, + "effectiveReviewerDelta": 8, + "evidenceDigest": "sha256:f01205e6060ee151fd289a95dbcf7980d47357160a93edd1bad26fc88b6b18ab" + } + ], + "stewardTasks": [ + { + "id": "task-review-r1-self-citation", + "reviewId": "review-r1-self-citation", + "projectId": "project-neuro-open", + "reviewer": "reviewer:orcid:0000-0001-reviewer-alpha", + "priority": "high", + "requiredAction": "remove-or-justify-coercive-citation-requests", + "blockedUpdates": [ + "reviewer-reputation", + "trusted-reviewer-badge", + "leaderboards", + "project-timeline" + ], + "flags": [ + "unsupported-self-citation-demand", + "off-topic-citation-cluster" + ], + "citationIds": [ + "cite-alpha-2024", + "cite-alpha-2023" + ] + }, + { + "id": "task-review-r2-reciprocal-pressure", + "reviewId": "review-r2-reciprocal-pressure", + "projectId": "project-neuro-open", + "reviewer": "anonymous-reviewer-42", + "priority": "high", + "requiredAction": "remove-or-justify-coercive-citation-requests", + "blockedUpdates": [ + "reviewer-reputation", + "trusted-reviewer-badge", + "leaderboards", + "project-timeline" + ], + "flags": [ + "reciprocal-citation-pressure" + ], + "citationIds": [ + "cite-mutual-2025" + ] + }, + { + "id": "task-review-r3-quota-pressure", + "reviewId": "review-r3-quota-pressure", + "projectId": "project-protein-open", + "reviewer": "reviewer:orcid:0000-0003-editorial-pressure", + "priority": "high", + "requiredAction": "remove-or-justify-coercive-citation-requests", + "blockedUpdates": [ + "reviewer-reputation", + "trusted-reviewer-badge", + "leaderboards", + "project-timeline" + ], + "flags": [ + "journal-citation-quota-pressure" + ], + "citationIds": [ + "cite-journal-quota-2026" + ] + } + ], + "authorProtections": [ + { + "id": "protect-project-neuro-open", + "projectId": "project-neuro-open", + "action": "block-author-penalty", + "reasons": [ + "citation-pressure-under-review" + ], + "blockedUpdates": [ + "author-response-penalty", + "project-quality-badge-decay", + "timeline-negative-review-event" + ], + "relatedReviewIds": [ + "review-r1-self-citation", + "review-r2-reciprocal-pressure" + ] + }, + { + "id": "protect-project-protein-open", + "projectId": "project-protein-open", + "action": "block-author-penalty", + "reasons": [ + "citation-pressure-under-review" + ], + "blockedUpdates": [ + "author-response-penalty", + "project-quality-badge-decay", + "timeline-negative-review-event" + ], + "relatedReviewIds": [ + "review-r3-quota-pressure" + ] + } + ], + "auditPacket": { + "packetId": "citation-coercion-review-batch-2026-05-28", + "generatedAt": "2026-05-28T12:30:00Z", + "events": [ + { + "type": "citation-pressure-held", + "reviewId": "review-r1-self-citation", + "projectId": "project-neuro-open", + "reviewer": "reviewer:orcid:0000-0001-reviewer-alpha", + "flags": [ + "unsupported-self-citation-demand", + "off-topic-citation-cluster" + ], + "decision": "steward-review-required" + }, + { + "type": "citation-pressure-held", + "reviewId": "review-r2-reciprocal-pressure", + "projectId": "project-neuro-open", + "reviewer": "anonymous-reviewer-42", + "flags": [ + "reciprocal-citation-pressure" + ], + "decision": "steward-review-required" + }, + { + "type": "citation-pressure-held", + "reviewId": "review-r3-quota-pressure", + "projectId": "project-protein-open", + "reviewer": "reviewer:orcid:0000-0003-editorial-pressure", + "flags": [ + "journal-citation-quota-pressure" + ], + "decision": "steward-review-required" + }, + { + "type": "citation-review-allowed", + "reviewId": "review-r4-legitimate-context", + "projectId": "project-microbiome-open", + "reviewer": "reviewer:orcid:0000-0004-methods-reviewer", + "flags": [], + "decision": "allow-reputation-update" + }, + { + "type": "steward-task-created", + "taskId": "task-review-r1-self-citation", + "reviewId": "review-r1-self-citation", + "projectId": "project-neuro-open", + "priority": "high" + }, + { + "type": "steward-task-created", + "taskId": "task-review-r2-reciprocal-pressure", + "reviewId": "review-r2-reciprocal-pressure", + "projectId": "project-neuro-open", + "priority": "high" + }, + { + "type": "steward-task-created", + "taskId": "task-review-r3-quota-pressure", + "reviewId": "review-r3-quota-pressure", + "projectId": "project-protein-open", + "priority": "high" + }, + { + "type": "author-protection-applied", + "projectId": "project-neuro-open", + "relatedReviewIds": [ + "review-r1-self-citation", + "review-r2-reciprocal-pressure" + ] + }, + { + "type": "author-protection-applied", + "projectId": "project-protein-open", + "relatedReviewIds": [ + "review-r3-quota-pressure" + ] + } + ], + "auditDigest": "sha256:637ed41ba3ea2b5506c1ee609c4ae56f1834c2670483849429566620b2402ecc" + }, + "summary": { + "totalReviews": 4, + "flaggedReviews": 3, + "heldReviewerCredit": 23, + "protectedProjects": 2, + "recommendedAction": "pause-coerced-citation-reputation-updates" + } +} diff --git a/peer-review-citation-coercion-guard/reports/citation-coercion-report.md b/peer-review-citation-coercion-guard/reports/citation-coercion-report.md new file mode 100644 index 00000000..1644b686 --- /dev/null +++ b/peer-review-citation-coercion-guard/reports/citation-coercion-report.md @@ -0,0 +1,34 @@ +# Peer Review Citation Coercion Guard + +Packet: citation-coercion-review-batch-2026-05-28 +Generated: 2026-05-28T12:30:00Z + +## Summary + +- Total reviews evaluated: 4 +- Flagged reviews: 3 +- Held reviewer credit: 23 +- Protected projects: 2 +- Recommended action: pause-coerced-citation-reputation-updates +- Audit digest: sha256:637ed41ba3ea2b5506c1ee609c4ae56f1834c2670483849429566620b2402ecc + +## Flagged Citation Pressure + +- review-r1-self-citation: unsupported-self-citation-demand, off-topic-citation-cluster +- review-r2-reciprocal-pressure: reciprocal-citation-pressure +- review-r3-quota-pressure: journal-citation-quota-pressure + +## Steward Tasks + +- task-review-r1-self-citation (high): remove-or-justify-coercive-citation-requests +- task-review-r2-reciprocal-pressure (high): remove-or-justify-coercive-citation-requests +- task-review-r3-quota-pressure (high): remove-or-justify-coercive-citation-requests + +## Author Protections + +- project-neuro-open: author-response-penalty, project-quality-badge-decay, timeline-negative-review-event +- project-protein-open: author-response-penalty, project-quality-badge-decay, timeline-negative-review-event + +## Privacy Notes + +Double-blind reviewer identifiers are replaced with anonymous labels before reports, tasks, and timeline events are emitted. The packet uses synthetic data only and does not contain credentials, private profile emails, live user data, external API calls, payment systems, or real review records. diff --git a/peer-review-citation-coercion-guard/reports/demo.mp4 b/peer-review-citation-coercion-guard/reports/demo.mp4 new file mode 100644 index 00000000..27d47269 Binary files /dev/null and b/peer-review-citation-coercion-guard/reports/demo.mp4 differ diff --git a/peer-review-citation-coercion-guard/reports/summary.svg b/peer-review-citation-coercion-guard/reports/summary.svg new file mode 100644 index 00000000..39cf2c33 --- /dev/null +++ b/peer-review-citation-coercion-guard/reports/summary.svg @@ -0,0 +1,12 @@ + + + + Peer Review Citation Coercion Guard + Flagged reviews: 3 / 4 + Held reviewer credit: 23 + Protected projects: 2 + Action: pause-coerced-citation-reputation-updates + Detects self-citation demands, reciprocal pressure, and journal quota coercion. + Blind reviewer labels are preserved without raw identity leakage. + sha256:637ed41ba3ea2b5506c1ee609c4ae56f1834c2670483849429566620b2402ecc + diff --git a/peer-review-citation-coercion-guard/requirements-map.md b/peer-review-citation-coercion-guard/requirements-map.md new file mode 100644 index 00000000..662ffbb4 --- /dev/null +++ b/peer-review-citation-coercion-guard/requirements-map.md @@ -0,0 +1,26 @@ +# Requirements Map + +## Peer Reviews And Comments + +- Structured peer reviews are evaluated before reputation and timeline updates. +- Public, semi-private, and double-blind review modes are represented. +- Citation requests are reviewed as part of peer-review quality and trust. +- Steward tasks preserve an auditable review history. + +## Contributor Credits + +- Reviewer credit is held when citation pressure may have biased the review. +- Legitimate citation suggestions still receive reviewer credit. +- Author-facing penalties are blocked while citation pressure is under review. + +## Reputation Scoring + +- Coercive citation findings block reviewer reputation deltas, trusted-reviewer badges, leaderboards, and project timeline updates. +- Clear steward tasks explain what must be removed or justified before credit is applied. +- The audit packet has a deterministic digest for transparent review. + +## Privacy And Trust + +- Double-blind reviewer IDs and private emails are not emitted in decisions, tasks, reports, or audit events. +- Synthetic data only; no private profile data, credentials, or external API calls. +- Author protections prevent coerced-citation disputes from harming project reputation. diff --git a/peer-review-citation-coercion-guard/test.js b/peer-review-citation-coercion-guard/test.js new file mode 100644 index 00000000..8eae4145 --- /dev/null +++ b/peer-review-citation-coercion-guard/test.js @@ -0,0 +1,101 @@ +const assert = require('assert'); +const { + evaluateCitationCoercion, + buildSampleReviewPacket +} = require('./index'); + +function byId(items, id) { + return items.find((item) => item.id === id); +} + +function testUnsupportedSelfCitationDemandTriggersStewardReview() { + const result = evaluateCitationCoercion(buildSampleReviewPacket()); + const decision = byId(result.reviewDecisions, 'review-r1-self-citation'); + + assert.equal(decision.decision, 'steward-review-required'); + assert.equal(decision.highestSeverity, 'high'); + assert.ok(decision.flags.includes('unsupported-self-citation-demand')); + assert.ok(decision.flags.includes('off-topic-citation-cluster')); + + const reputationHold = byId(result.reputationActions, 'hold-review-r1-self-citation'); + assert.equal(reputationHold.action, 'hold-reviewer-credit'); + assert.equal(reputationHold.effectiveReviewerDelta, 0); + assert.deepEqual(reputationHold.blockedUpdates, [ + 'reviewer-reputation', + 'trusted-reviewer-badge', + 'leaderboards', + 'project-timeline' + ]); +} + +function testReciprocalCitationPressureIsFlaggedWithoutPunishingAuthors() { + const result = evaluateCitationCoercion(buildSampleReviewPacket()); + const decision = byId(result.reviewDecisions, 'review-r2-reciprocal-pressure'); + + assert.equal(decision.decision, 'steward-review-required'); + assert.ok(decision.flags.includes('reciprocal-citation-pressure')); + + const authorProtection = byId(result.authorProtections, 'protect-project-neuro-open'); + assert.equal(authorProtection.action, 'block-author-penalty'); + assert.ok(authorProtection.reasons.includes('citation-pressure-under-review')); +} + +function testJournalQuotaPressureBlocksReputationChanges() { + const result = evaluateCitationCoercion(buildSampleReviewPacket()); + const decision = byId(result.reviewDecisions, 'review-r3-quota-pressure'); + + assert.ok(decision.flags.includes('journal-citation-quota-pressure')); + assert.equal(decision.requiredStewardAction, 'review-citation-demands-before-score-or-badge-update'); + + const task = byId(result.stewardTasks, 'task-review-r3-quota-pressure'); + assert.equal(task.priority, 'high'); + assert.equal(task.requiredAction, 'remove-or-justify-coercive-citation-requests'); +} + +function testLegitimateCitationSuggestionStaysAllowed() { + const result = evaluateCitationCoercion(buildSampleReviewPacket()); + const decision = byId(result.reviewDecisions, 'review-r4-legitimate-context'); + + assert.equal(decision.decision, 'allow-reputation-update'); + assert.deepEqual(decision.flags, []); + + const action = byId(result.reputationActions, 'apply-review-r4-legitimate-context'); + assert.equal(action.action, 'apply-reviewer-credit'); + assert.equal(action.effectiveReviewerDelta, 8); +} + +function testBlindReviewerIdentityIsRedactedFromOutputs() { + const result = evaluateCitationCoercion(buildSampleReviewPacket()); + const serialized = JSON.stringify(result); + + assert.ok(serialized.includes('anonymous-reviewer-42')); + assert.ok(!serialized.includes('orcid:0000-0002-private-pressure')); + assert.ok(!serialized.includes('private-pressure@example.edu')); +} + +function testSummaryAndAuditDigestAreDeterministic() { + const first = evaluateCitationCoercion(buildSampleReviewPacket()); + const second = evaluateCitationCoercion(buildSampleReviewPacket()); + + assert.equal(first.summary.totalReviews, 4); + assert.equal(first.summary.flaggedReviews, 3); + assert.equal(first.summary.heldReviewerCredit, 23); + assert.equal(first.summary.recommendedAction, 'pause-coerced-citation-reputation-updates'); + assert.equal(first.auditPacket.auditDigest, second.auditPacket.auditDigest); + assert.ok(first.auditPacket.auditDigest.startsWith('sha256:')); +} + +const tests = [ + testUnsupportedSelfCitationDemandTriggersStewardReview, + testReciprocalCitationPressureIsFlaggedWithoutPunishingAuthors, + testJournalQuotaPressureBlocksReputationChanges, + testLegitimateCitationSuggestionStaysAllowed, + testBlindReviewerIdentityIsRedactedFromOutputs, + testSummaryAndAuditDigestAreDeterministic +]; + +for (const test of tests) { + test(); +} + +console.log(`${tests.length} peer-review citation coercion guard tests passed`);