From e5c7e9786b02189b9ee93a5a76fdffbd9424675f Mon Sep 17 00:00:00 2001 From: Telemark Codex Agent Date: Sat, 15 Aug 2026 23:38:25 +0200 Subject: [PATCH] Add review retaliation guard --- review-retaliation-guard/README.md | 47 +++ .../data/sample_reputation_events.json | 156 +++++++++ review-retaliation-guard/package.json | 13 + review-retaliation-guard/reports/demo.mp4 | Bin 0 -> 51428 bytes .../reports/reviewer-packet.md | 72 ++++ review-retaliation-guard/reports/summary.json | 241 +++++++++++++ review-retaliation-guard/reports/summary.svg | 17 + review-retaliation-guard/scripts/demo.js | 82 +++++ .../scripts/render-demo-video.js | 60 ++++ review-retaliation-guard/src/index.js | 328 ++++++++++++++++++ .../test/review-retaliation-guard.test.js | 43 +++ 11 files changed, 1059 insertions(+) create mode 100644 review-retaliation-guard/README.md create mode 100644 review-retaliation-guard/data/sample_reputation_events.json create mode 100644 review-retaliation-guard/package.json create mode 100644 review-retaliation-guard/reports/demo.mp4 create mode 100644 review-retaliation-guard/reports/reviewer-packet.md create mode 100644 review-retaliation-guard/reports/summary.json create mode 100644 review-retaliation-guard/reports/summary.svg create mode 100644 review-retaliation-guard/scripts/demo.js create mode 100644 review-retaliation-guard/scripts/render-demo-video.js create mode 100644 review-retaliation-guard/src/index.js create mode 100644 review-retaliation-guard/test/review-retaliation-guard.test.js diff --git a/review-retaliation-guard/README.md b/review-retaliation-guard/README.md new file mode 100644 index 00000000..03d522ea --- /dev/null +++ b/review-retaliation-guard/README.md @@ -0,0 +1,47 @@ +# Review Retaliation Guard + +This contribution adds a focused guardrail for SCIBASE's Community & User Reputation System. It detects reputation actions that may punish a reviewer after an unfavorable manuscript review, freezes unsafe public reputation exports, and produces an evidence packet for independent moderation. + +The slice is intentionally narrow: it does not build a full reputation system. It covers the trust gap between peer-review outcomes and reputation changes, where author-affiliated actors could downvote, remove badges, withdraw endorsements, or reduce profile visibility after a critical review. + +## What It Checks + +- Whether the underlying peer review is critical through recommendation, rating, tone, or criticality score. +- Whether a punitive reputation event targets the reviewer inside a configurable retaliation window. +- Whether the actor is affiliated with the manuscript, lab, institution, funder, or study team. +- Whether multiple affiliated actors create a clustered penalty burst. +- Whether messages contain coercive language asking the reviewer to soften or revise criticism. +- Whether public profile or leaderboard exports are still enabled while unresolved retaliation risk exists. +- Whether the reviewer has independent moderation and a meaningful appeal window before reputation deltas apply. + +## Running Locally + +```bash +npm test +npm run demo +npm run check +npm run video +``` + +The demo uses only synthetic sample packets in `data/sample_reputation_events.json`. + +Generated files: + +- `reports/summary.json` +- `reports/reviewer-packet.md` +- `reports/summary.svg` +- `reports/demo.mp4` + +The MP4 is generated from an FFmpeg color source and drawtext slate. It is not a screen recording and contains no desktop capture or private information. + +## Why This Matters + +Reputation systems can unintentionally chill rigorous peer review when negative reputation changes are allowed to follow critical reviews without context. A reviewer who recommends rejection should not lose badges, endorsements, public visibility, or leaderboard standing because authors or affiliated collaborators dislike the review outcome. + +This guard gives SCIBASE an auditable path: + +1. Freeze reputation deltas tied to the flagged review. +2. Preserve event evidence and coercive messages. +3. Require independent moderator review. +4. Notify the affected reviewer of appeal options. +5. Resume public reputation export only after resolution. diff --git a/review-retaliation-guard/data/sample_reputation_events.json b/review-retaliation-guard/data/sample_reputation_events.json new file mode 100644 index 00000000..f3dce3a1 --- /dev/null +++ b/review-retaliation-guard/data/sample_reputation_events.json @@ -0,0 +1,156 @@ +[ + { + "id": "rep-clear-001", + "title": "Critical review with independent reputation hold", + "reviewDate": "2026-08-01", + "review": { + "reviewerId": "reviewer-17", + "recommendation": "major_revision", + "rating": 2, + "criticalityScore": 0.82 + }, + "manuscript": { + "authors": ["author-a", "author-b"], + "institutions": ["North Ridge Lab"] + }, + "reputationEvents": [ + { + "id": "evt-001", + "createdAt": "2026-08-04", + "actor": "moderator-3", + "actorRelation": "independent_moderator", + "targetReviewer": "reviewer-17", + "action": "audit_hold", + "reputationDelta": 0, + "reason": "Automatic freeze while author response is reviewed." + } + ], + "safeguards": { + "retaliationWindowDays": 35, + "independentModeratorReview": true, + "appealWindowDays": 21, + "freezePublicExport": true + }, + "exportTarget": { + "publicProfileExport": false + } + }, + { + "id": "rep-hold-002", + "title": "Author-affiliated penalties after rejection recommendation", + "reviewDate": "2026-08-02", + "review": { + "reviewerId": "reviewer-88", + "recommendation": "reject", + "rating": 1, + "criticalityScore": 0.95 + }, + "manuscript": { + "authors": ["author-c", "author-d"], + "institutions": ["Cedar Clinical Center"] + }, + "reputationEvents": [ + { + "id": "evt-011", + "createdAt": "2026-08-05", + "actor": "author-c", + "actorRelation": "author", + "targetReviewer": "reviewer-88", + "action": "downvote", + "reputationDelta": -18, + "reason": "Low quality review." + }, + { + "id": "evt-012", + "createdAt": "2026-08-06", + "actor": "lab-manager-c", + "actorRelation": "same_lab", + "targetReviewer": "reviewer-88", + "action": "badge_removed", + "reputationDelta": -7, + "reason": "Community confidence issue." + } + ], + "safeguards": { + "retaliationWindowDays": 35, + "independentModeratorReview": false, + "appealWindowDays": 7, + "freezePublicExport": false + }, + "exportTarget": { + "publicProfileExport": true + } + }, + { + "id": "rep-hold-003", + "title": "Coercive endorsement withdrawal tied to review revision", + "reviewDate": "2026-08-03", + "review": { + "reviewerId": "reviewer-42", + "recommendation": "major revisions", + "rating": 2, + "tone": "critical" + }, + "manuscript": { + "authors": ["author-q"], + "institutions": ["East Harbor University"] + }, + "reputationEvents": [ + { + "id": "evt-021", + "createdAt": "2026-08-04", + "actor": "author-q", + "actorRelation": "author", + "targetReviewer": "reviewer-42", + "action": "endorsement_withdrawn", + "reputationDelta": -12, + "message": "Unless you revise the review and remove the negative comment, your reputation will drop." + } + ], + "safeguards": { + "retaliationWindowDays": 35, + "independentModeratorReview": false, + "appealWindowDays": 0, + "freezePublicExport": false + }, + "exportTarget": { + "publicProfileExport": true + } + }, + { + "id": "rep-clear-004", + "title": "Unrelated old moderation action outside review window", + "reviewDate": "2026-08-10", + "review": { + "reviewerId": "reviewer-9", + "recommendation": "accept", + "rating": 5, + "criticalityScore": 0.1 + }, + "manuscript": { + "authors": ["author-z"], + "institutions": ["Lakeview Bioinformatics"] + }, + "reputationEvents": [ + { + "id": "evt-031", + "createdAt": "2026-07-01", + "actor": "trust-and-safety", + "actorRelation": "independent_moderator", + "targetReviewer": "reviewer-9", + "action": "account_flag", + "reputationDelta": -3, + "reason": "Old spam duplicate unrelated to manuscript review." + } + ], + "safeguards": { + "retaliationWindowDays": 35, + "independentModeratorReview": true, + "appealWindowDays": 21, + "freezePublicExport": true + }, + "exportTarget": { + "publicProfileExport": false + } + } +] diff --git a/review-retaliation-guard/package.json b/review-retaliation-guard/package.json new file mode 100644 index 00000000..c9e96d0e --- /dev/null +++ b/review-retaliation-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "review-retaliation-guard", + "version": "1.0.0", + "description": "Dependency-free reputation guard for detecting retaliation after critical peer review.", + "main": "src/index.js", + "scripts": { + "test": "node --test", + "demo": "node scripts/demo.js", + "video": "node scripts/render-demo-video.js", + "check": "node --check src/index.js && node --check scripts/demo.js && node --check scripts/render-demo-video.js" + }, + "license": "MIT" +} diff --git a/review-retaliation-guard/reports/demo.mp4 b/review-retaliation-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..7270fc5d9ee2f8467ea17bb668914d176d42ce28 GIT binary patch literal 51428 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 + + Review Retaliation Guard + Synthetic reputation-event packet audit for SCIBASE community trust. + Decision distribution + HOLD + + 2 + REVIEW + + 0 + CLEAR + + 2 + Flags affiliated punitive actions, coercive messages, clustered retaliation, and unsafe public profile export. + Synthetic generated artifact only. No desktop capture or private data. + diff --git a/review-retaliation-guard/scripts/demo.js b/review-retaliation-guard/scripts/demo.js new file mode 100644 index 00000000..d0b2ab07 --- /dev/null +++ b/review-retaliation-guard/scripts/demo.js @@ -0,0 +1,82 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { analyzeReputationPackets } = require("../src"); + +const root = path.resolve(__dirname, ".."); +const dataPath = path.join(root, "data", "sample_reputation_events.json"); +const outDir = path.join(root, "reports"); +const packets = JSON.parse(fs.readFileSync(dataPath, "utf8")); +const report = analyzeReputationPackets(packets); + +fs.mkdirSync(outDir, { recursive: true }); +fs.writeFileSync(path.join(outDir, "summary.json"), `${JSON.stringify(report, null, 2)}\n`); + +const markdown = [ + "# Review Retaliation Guard Report", + "", + `Generated: ${report.generatedAt}`, + `Packets analyzed: ${report.totalPackets}`, + `Decision counts: CLEAR ${report.counts.CLEAR}, REVIEW ${report.counts.REVIEW}, HOLD ${report.counts.HOLD}`, + "", + "## Findings", + "", + ...report.results.flatMap((item) => [ + `### ${item.id}: ${item.title}`, + "", + `Decision: ${item.decision}`, + `Risk score: ${item.riskScore}`, + `Critical review: ${item.criticalReview ? "yes" : "no"}`, + `Suspicious events: ${item.suspiciousEventCount}`, + "", + item.findings.length === 0 + ? "- No retaliation risk detected." + : item.findings.map((finding) => `- ${finding.severity.toUpperCase()} ${finding.code}: ${finding.message}`).join("\n"), + "", + "Recommended actions:", + ...item.recommendedActions.map((action) => `- ${action}`), + "" + ]) +].join("\n"); + +fs.writeFileSync(path.join(outDir, "reviewer-packet.md"), markdown); + +const maxBar = 620; +const hold = report.counts.HOLD; +const review = report.counts.REVIEW; +const clear = report.counts.CLEAR; +const total = Math.max(1, report.totalPackets); +const holdWidth = Math.round((hold / total) * maxBar); +const reviewWidth = Math.round((review / total) * maxBar); +const clearWidth = Math.round((clear / total) * maxBar); +const svg = ` + + Review Retaliation Guard + Synthetic reputation-event packet audit for SCIBASE community trust. + Decision distribution + HOLD + + ${hold} + REVIEW + + ${review} + CLEAR + + ${clear} + Flags affiliated punitive actions, coercive messages, clustered retaliation, and unsafe public profile export. + Synthetic generated artifact only. No desktop capture or private data. + +`; + +fs.writeFileSync(path.join(outDir, "summary.svg"), svg); + +console.log(JSON.stringify({ + packets: report.totalPackets, + counts: report.counts, + outputs: [ + path.join(outDir, "summary.json"), + path.join(outDir, "reviewer-packet.md"), + path.join(outDir, "summary.svg") + ] +}, null, 2)); diff --git a/review-retaliation-guard/scripts/render-demo-video.js b/review-retaliation-guard/scripts/render-demo-video.js new file mode 100644 index 00000000..e42952ff --- /dev/null +++ b/review-retaliation-guard/scripts/render-demo-video.js @@ -0,0 +1,60 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const root = path.resolve(__dirname, ".."); +const ffmpeg = path.resolve( + root, + "..", + "..", + "tool_downloads", + "video_tools", + "node_modules", + "ffmpeg-static", + "ffmpeg.exe" +); +const outDir = path.join(root, "reports"); +const out = path.join(outDir, "demo.mp4"); +const font = "C\\:/Windows/Fonts/arial.ttf"; + +fs.mkdirSync(outDir, { recursive: true }); + +const draw = [ + `drawtext=fontfile=${font}:text='Review Retaliation Guard':x=64:y=56:fontsize=44:fontcolor=black`, + `drawtext=fontfile=${font}:text='Community reputation protection after critical peer review':x=64:y=122:fontsize=26:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='Synthetic reputation packets analyzed 4':x=64:y=214:fontsize=34:fontcolor=black`, + `drawtext=fontfile=${font}:text='HOLD 2 REVIEW 0 CLEAR 2':x=64:y=274:fontsize=40:fontcolor=0x245fd6`, + `drawtext=fontfile=${font}:text='Detects author-affiliated penalties and clustered retaliation':x=64:y=366:fontsize=27:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='Freezes public reputation exports until independent review':x=64:y=428:fontsize=27:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='Synthetic generated slate only no desktop capture or private data':x=64:y=492:fontsize=26:fontcolor=0x34413b` +].join(","); + +if (!fs.existsSync(ffmpeg)) { + throw new Error(`ffmpeg binary not found at ${ffmpeg}`); +} + +const result = spawnSync(ffmpeg, [ + "-y", + "-f", + "lavfi", + "-i", + "color=c=0xf9faf8:s=1280x720:d=8:r=30", + "-vf", + draw, + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + out +], { encoding: "utf8" }); + +if (result.status !== 0) { + process.stderr.write(result.stderr || result.stdout); + process.exit(result.status || 1); +} + +console.log(JSON.stringify({ out, bytes: fs.statSync(out).size }, null, 2)); diff --git a/review-retaliation-guard/src/index.js b/review-retaliation-guard/src/index.js new file mode 100644 index 00000000..42c70ed4 --- /dev/null +++ b/review-retaliation-guard/src/index.js @@ -0,0 +1,328 @@ +"use strict"; + +const CRITICAL_RECOMMENDATIONS = new Set([ + "reject", + "major_revision", + "major revisions", + "not_recommended", + "do_not_accept" +]); + +const PUNITIVE_ACTIONS = new Set([ + "downvote", + "reputation_penalty", + "badge_removed", + "endorsement_withdrawn", + "role_revoked", + "account_flag", + "reviewer_blocked", + "profile_visibility_reduced" +]); + +const COERCION_PATTERNS = [ + /\bchange\s+your\s+review\b/i, + /\bwithdraw\s+your\s+criticism\b/i, + /\bremove\s+the\s+negative\s+comment\b/i, + /\bunless\s+you\s+revise\b/i, + /\breputation\s+will\s+drop\b/i, + /\bwe\s+will\s+penalize\b/i +]; + +function list(value) { + return Array.isArray(value) ? value : []; +} + +function normalizeText(value) { + return String(value || "").trim().toLowerCase(); +} + +function parseDate(value) { + const time = Date.parse(value); + return Number.isFinite(time) ? new Date(time) : null; +} + +function daysBetween(startValue, endValue) { + const start = parseDate(startValue); + const end = parseDate(endValue); + if (!start || !end) return null; + return Math.round((end.getTime() - start.getTime()) / 86400000); +} + +function finding(code, severity, message, evidence, remediation) { + return { code, severity, message, evidence, remediation }; +} + +function normalizePacket(packet) { + return { + id: packet.id, + title: packet.title || "Untitled reputation packet", + reviewDate: packet.reviewDate || packet.review?.submittedAt || "2026-08-15", + review: packet.review || {}, + reputationEvents: list(packet.reputationEvents), + manuscript: packet.manuscript || {}, + safeguards: packet.safeguards || {}, + exportTarget: packet.exportTarget || {} + }; +} + +function isCriticalReview(review) { + const recommendation = normalizeText(review.recommendation).replace(/\s+/g, "_"); + if (CRITICAL_RECOMMENDATIONS.has(recommendation)) return true; + if (review.rating !== undefined && Number(review.rating) <= 2) return true; + if (Number(review.criticalityScore || 0) >= 0.7) return true; + return normalizeText(review.tone) === "critical"; +} + +function actorRelation(event, packet) { + const relation = normalizeText(event.actorRelation); + if (relation) return relation; + + const authors = new Set(list(packet.manuscript.authors).map(normalizeText)); + const institutions = new Set(list(packet.manuscript.institutions).map(normalizeText)); + const actor = normalizeText(event.actor); + const institution = normalizeText(event.actorInstitution); + + if (authors.has(actor)) return "author"; + if (institution && institutions.has(institution)) return "same_institution"; + return "unaffiliated"; +} + +function isAffiliatedRelation(relation) { + return [ + "author", + "coauthor", + "same_institution", + "same_lab", + "project_collaborator", + "funder", + "study_team" + ].includes(relation); +} + +function isPunitiveEvent(event) { + if (PUNITIVE_ACTIONS.has(normalizeText(event.action))) return true; + return Number(event.reputationDelta || 0) < 0; +} + +function targetsReviewer(event, reviewerId) { + const reviewer = normalizeText(reviewerId); + if (!reviewer) return false; + return [ + event.targetReviewer, + event.targetReviewerId, + event.targetUser, + event.targetContributionOwner + ].some((value) => normalizeText(value) === reviewer); +} + +function hasCoerciveLanguage(event) { + const text = `${event.note || ""} ${event.message || ""} ${event.reason || ""}`; + return COERCION_PATTERNS.some((pattern) => pattern.test(text)); +} + +function scoreFindings(findings) { + const score = findings.reduce((total, item) => { + if (item.severity === "critical") return total + 35; + if (item.severity === "major") return total + 22; + return total + 10; + }, 0); + return Math.min(100, score); +} + +function decisionFor(findings) { + if (findings.some((item) => item.severity === "critical")) return "HOLD"; + if (findings.some((item) => item.severity === "major")) return "REVIEW"; + return "CLEAR"; +} + +function analyzePacket(input) { + const packet = normalizePacket(input); + const findings = []; + const reviewerId = packet.review.reviewerId || packet.review.reviewer; + const criticalReview = isCriticalReview(packet.review); + const retaliationWindowDays = Number(packet.safeguards.retaliationWindowDays || 35); + + const relevantEvents = packet.reputationEvents.map((event) => { + const relation = actorRelation(event, packet); + const daysAfterReview = daysBetween(packet.reviewDate, event.createdAt); + return { + ...event, + relation, + daysAfterReview, + punitive: isPunitiveEvent(event), + targetsReviewer: targetsReviewer(event, reviewerId), + coerciveLanguage: hasCoerciveLanguage(event), + inRetaliationWindow: daysAfterReview !== null && daysAfterReview >= 0 && daysAfterReview <= retaliationWindowDays + }; + }); + + const suspiciousEvents = relevantEvents.filter((event) => ( + criticalReview && + event.punitive && + event.targetsReviewer && + event.inRetaliationWindow && + isAffiliatedRelation(event.relation) + )); + + for (const event of suspiciousEvents) { + findings.push(finding( + "AFFILIATED_PUNITIVE_ACTION_AFTER_CRITICAL_REVIEW", + "critical", + "An author-affiliated actor applied a punitive reputation action shortly after a critical review.", + { + action: event.action, + actor: event.actor, + relation: event.relation, + targetReviewer: reviewerId, + daysAfterReview: event.daysAfterReview, + reputationDelta: event.reputationDelta || 0 + }, + "Freeze the reputation delta and route the action to an independent moderator before publication or profile export." + )); + } + + const coerciveEvents = relevantEvents.filter((event) => ( + event.targetsReviewer && + event.inRetaliationWindow && + event.coerciveLanguage + )); + + for (const event of coerciveEvents) { + findings.push(finding( + "COERCIVE_REVIEW_REVISION_SIGNAL", + "critical", + "A reputation event contains language that appears to pressure the reviewer to soften or alter criticism.", + { + actor: event.actor, + action: event.action, + daysAfterReview: event.daysAfterReview, + excerpt: String(event.message || event.note || event.reason || "").slice(0, 160) + }, + "Separate reputation handling from manuscript-review negotiation and preserve the message for moderator review." + )); + } + + const punitiveActorCount = new Set(suspiciousEvents.map((event) => normalizeText(event.actor))).size; + if (punitiveActorCount >= 2) { + findings.push(finding( + "RECIPROCAL_OR_CLUSTERED_RETALIATION_BURST", + "critical", + "Multiple affiliated actors targeted the same reviewer within the retaliation window.", + { + affectedReviewer: reviewerId, + distinctActors: punitiveActorCount, + events: suspiciousEvents.map((event) => ({ actor: event.actor, action: event.action, daysAfterReview: event.daysAfterReview })) + }, + "Escalate as a retaliation cluster, suspend automated scoring, and require independent adjudication." + )); + } + + const badgeWithdrawals = suspiciousEvents.filter((event) => [ + "badge_removed", + "endorsement_withdrawn", + "role_revoked", + "profile_visibility_reduced" + ].includes(normalizeText(event.action))); + + if (badgeWithdrawals.length > 0) { + findings.push(finding( + "PROFILE_CREDIT_REMOVED_AFTER_UNFAVORABLE_REVIEW", + "major", + "A public-facing badge, endorsement, role, or visibility signal was reduced after an unfavorable review.", + { events: badgeWithdrawals.map((event) => ({ actor: event.actor, action: event.action, daysAfterReview: event.daysAfterReview })) }, + "Hold public profile updates until the action is justified by evidence unrelated to the review outcome." + )); + } + + if (criticalReview && suspiciousEvents.length > 0 && packet.safeguards.independentModeratorReview !== true) { + findings.push(finding( + "INDEPENDENT_REVIEW_MISSING_FOR_RETALIATION_RISK", + "major", + "The packet has retaliation risk but no independent moderator review recorded.", + { independentModeratorReview: packet.safeguards.independentModeratorReview || false }, + "Add independent moderator signoff before applying the reputation change." + )); + } + + const appealWindowDays = Number(packet.safeguards.appealWindowDays || 0); + if (suspiciousEvents.length > 0 && appealWindowDays < 14) { + findings.push(finding( + "APPEAL_WINDOW_TOO_SHORT_FOR_REPUTATION_DELTA", + "major", + "The reviewer has insufficient time to appeal a reputation-impacting action.", + { appealWindowDays }, + "Provide at least a 14-day appeal window and show the evidence packet to the affected reviewer." + )); + } + + if ( + findings.length > 0 && + packet.exportTarget.publicProfileExport === true && + packet.safeguards.freezePublicExport !== true + ) { + findings.push(finding( + "PUBLIC_REPUTATION_EXPORT_NOT_FROZEN", + "critical", + "Public reputation export is enabled while unresolved retaliation risk exists.", + { publicProfileExport: packet.exportTarget.publicProfileExport, freezePublicExport: packet.safeguards.freezePublicExport || false }, + "Freeze the public export and leaderboard deltas until the retaliation review is resolved." + )); + } + + const riskScore = scoreFindings(findings); + const decision = decisionFor(findings); + + return { + id: packet.id, + title: packet.title, + reviewerId, + criticalReview, + retaliationWindowDays, + decision, + riskScore, + findings, + suspiciousEventCount: suspiciousEvents.length, + relevantEventCount: relevantEvents.length, + recommendedActions: buildRecommendedActions(decision, findings) + }; +} + +function buildRecommendedActions(decision, findings) { + if (decision === "CLEAR") { + return ["Apply reputation changes with normal audit logging."]; + } + + const actions = [ + "Freeze reputation deltas tied to the flagged review.", + "Generate an evidence packet for an independent moderator.", + "Notify the affected reviewer of the appeal pathway." + ]; + + if (findings.some((item) => item.code === "PUBLIC_REPUTATION_EXPORT_NOT_FROZEN")) { + actions.unshift("Disable leaderboard and public profile export for the affected reviewer until resolution."); + } + + return actions; +} + +function analyzeReputationPackets(packets) { + const results = list(packets).map(analyzePacket); + const counts = results.reduce((acc, item) => { + acc[item.decision] = (acc[item.decision] || 0) + 1; + return acc; + }, { CLEAR: 0, REVIEW: 0, HOLD: 0 }); + + return { + generatedAt: new Date().toISOString(), + totalPackets: results.length, + counts, + results + }; +} + +module.exports = { + analyzePacket, + analyzeReputationPackets, + isCriticalReview, + daysBetween +}; diff --git a/review-retaliation-guard/test/review-retaliation-guard.test.js b/review-retaliation-guard/test/review-retaliation-guard.test.js new file mode 100644 index 00000000..e957acf1 --- /dev/null +++ b/review-retaliation-guard/test/review-retaliation-guard.test.js @@ -0,0 +1,43 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); +const { analyzePacket, analyzeReputationPackets, daysBetween, isCriticalReview } = require("../src"); +const samples = require("../data/sample_reputation_events.json"); + +test("detects critical review signals", () => { + assert.equal(isCriticalReview({ recommendation: "reject", rating: 5 }), true); + assert.equal(isCriticalReview({ recommendation: "accept", rating: 1 }), true); + assert.equal(isCriticalReview({ recommendation: "accept", rating: 5 }), false); +}); + +test("computes days between review and reputation event", () => { + assert.equal(daysBetween("2026-08-01", "2026-08-04"), 3); + assert.equal(daysBetween("not-a-date", "2026-08-04"), null); +}); + +test("holds affiliated punitive actions after a critical review", () => { + const result = analyzePacket(samples.find((item) => item.id === "rep-hold-002")); + assert.equal(result.decision, "HOLD"); + assert.equal(result.suspiciousEventCount, 2); + assert.ok(result.findings.some((item) => item.code === "AFFILIATED_PUNITIVE_ACTION_AFTER_CRITICAL_REVIEW")); + assert.ok(result.findings.some((item) => item.code === "RECIPROCAL_OR_CLUSTERED_RETALIATION_BURST")); + assert.ok(result.findings.some((item) => item.code === "PUBLIC_REPUTATION_EXPORT_NOT_FROZEN")); +}); + +test("detects coercive language in a reputation event", () => { + const result = analyzePacket(samples.find((item) => item.id === "rep-hold-003")); + assert.equal(result.decision, "HOLD"); + assert.ok(result.findings.some((item) => item.code === "COERCIVE_REVIEW_REVISION_SIGNAL")); +}); + +test("does not flag independent freezes or unrelated old moderation", () => { + assert.equal(analyzePacket(samples.find((item) => item.id === "rep-clear-001")).decision, "CLEAR"); + assert.equal(analyzePacket(samples.find((item) => item.id === "rep-clear-004")).decision, "CLEAR"); +}); + +test("aggregates packet decisions for reviewer-facing reports", () => { + const report = analyzeReputationPackets(samples); + assert.equal(report.totalPackets, 4); + assert.deepEqual(report.counts, { CLEAR: 2, REVIEW: 0, HOLD: 2 }); +});