From 9440f93379913e7efd05717b6e17b75ff335a440 Mon Sep 17 00:00:00 2001 From: Julian Ahlmark Date: Fri, 29 May 2026 15:13:59 +0300 Subject: [PATCH] Add reputation review abuse guard --- reputation-review-abuse-guard/demo.js | 56 ++++++++++++++ reputation-review-abuse-guard/index.js | 68 +++++++++++++++++ reputation-review-abuse-guard/readme.md | 33 ++++++++ reputation-review-abuse-guard/render-video.js | 45 +++++++++++ .../reports/demo.mp4 | Bin 0 -> 7802 bytes .../reports/review-abuse-report.json | 71 ++++++++++++++++++ .../reports/review-abuse-report.md | 41 ++++++++++ .../reports/summary.svg | 21 ++++++ reputation-review-abuse-guard/sample-data.js | 61 +++++++++++++++ reputation-review-abuse-guard/test.js | 20 +++++ 10 files changed, 416 insertions(+) create mode 100644 reputation-review-abuse-guard/demo.js create mode 100644 reputation-review-abuse-guard/index.js create mode 100644 reputation-review-abuse-guard/readme.md create mode 100644 reputation-review-abuse-guard/render-video.js create mode 100644 reputation-review-abuse-guard/reports/demo.mp4 create mode 100644 reputation-review-abuse-guard/reports/review-abuse-report.json create mode 100644 reputation-review-abuse-guard/reports/review-abuse-report.md create mode 100644 reputation-review-abuse-guard/reports/summary.svg create mode 100644 reputation-review-abuse-guard/sample-data.js create mode 100644 reputation-review-abuse-guard/test.js diff --git a/reputation-review-abuse-guard/demo.js b/reputation-review-abuse-guard/demo.js new file mode 100644 index 00000000..b810e3b3 --- /dev/null +++ b/reputation-review-abuse-guard/demo.js @@ -0,0 +1,56 @@ +const fs = require("fs"); +const path = require("path"); +const { reputationBatch } = require("./sample-data"); +const { evaluateBatch } = require("./index"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const report = evaluateBatch(reputationBatch); +fs.writeFileSync(path.join(reportsDir, "review-abuse-report.json"), `${JSON.stringify(report, null, 2)}\n`); + +const markdown = [ + "# Reputation Review-Abuse Guard Report", + "", + `Batch: ${report.batchId}`, + `Generated: ${report.generatedAt}`, + "", + "## Summary", + "", + `- Reviews evaluated: ${report.summary.total}`, + `- Credits counted: ${report.summary["count-reputation-credit"] || 0}`, + `- Credits held: ${report.summary["hold-reputation-credit"] || 0}`, + `- Findings: ${report.summary.findingCount}`, + "", + "## Decisions", + "", + ...report.reviews.flatMap((review) => [ + `### ${review.id}`, + "", + `- Reviewer: ${review.reviewer}`, + `- Decision: ${review.decision}`, + `- Findings: ${review.findings.length ? review.findings.join("; ") : "none"}`, + `- Actions: ${review.actions.join("; ")}`, + "" + ]) +].join("\n").trim(); + +fs.writeFileSync(path.join(reportsDir, "review-abuse-report.md"), `${markdown}\n`); + +const svg = ` + + Review-Abuse Reputation Guard + Peer review credit moderation before leaderboard updates + ${report.reviews.map((review, index) => { + const y = 150 + index * 82; + const color = review.decision === "count-reputation-credit" ? "#047857" : "#b45309"; + return ` + ${review.id} + ${review.decision} + ${review.findings.length} finding(s), delta ${review.scoreDelta}`; + }).join("\n ")} + +`; + +fs.writeFileSync(path.join(reportsDir, "summary.svg"), svg); +console.log(JSON.stringify(report.summary, null, 2)); diff --git a/reputation-review-abuse-guard/index.js b/reputation-review-abuse-guard/index.js new file mode 100644 index 00000000..7b155ef5 --- /dev/null +++ b/reputation-review-abuse-guard/index.js @@ -0,0 +1,68 @@ +const MIN_REVIEW_WORDS = 40; +const HIGH_REPUTATION_DELTA = 10; + +function evaluateReview(review) { + const findings = []; + + if (review.reviewerAffiliation === review.authorAffiliation) { + findings.push("reviewer shares affiliation with target author"); + } + if (review.commentWords < MIN_REVIEW_WORDS && review.scoreDelta >= HIGH_REPUTATION_DELTA) { + findings.push("high reputation delta from low-effort review text"); + } + if (review.reciprocalReviewIds.length > 0) { + findings.push(`reciprocal review link detected: ${review.reciprocalReviewIds.join(", ")}`); + } + if (review.anonymousMode && review.leakedIdentityText) { + findings.push("anonymous review contains identity leakage"); + } + if (review.endorsementCluster.includes(review.reviewer) && review.endorsementCluster.includes(review.targetAuthor)) { + findings.push("reviewer and target author appear in same endorsement cluster"); + } + + let decision = "count-reputation-credit"; + if (findings.some((finding) => finding.includes("identity leakage"))) { + decision = "redact-before-credit"; + } + if (findings.length >= 2) { + decision = "hold-reputation-credit"; + } + + const actions = { + "count-reputation-credit": ["apply reputation credit", "record review audit"], + "redact-before-credit": ["redact identity leakage", "queue moderator check before credit"], + "hold-reputation-credit": ["hold reputation credit", "route to trust-and-safety review", "suppress leaderboard update"] + }[decision]; + + return { + id: review.id, + reviewer: review.reviewer, + targetProject: review.targetProject, + decision, + scoreDelta: review.scoreDelta, + findings, + actions + }; +} + +function evaluateBatch(batch) { + const reviews = batch.reviews.map(evaluateReview); + const summary = reviews.reduce( + (acc, review) => { + acc.total += 1; + acc[review.decision] = (acc[review.decision] || 0) + 1; + acc.findingCount += review.findings.length; + return acc; + }, + { total: 0, findingCount: 0 } + ); + + return { + batchId: batch.id, + generatedAt: batch.generatedAt, + summary, + reviews + }; +} + +module.exports = { MIN_REVIEW_WORDS, HIGH_REPUTATION_DELTA, evaluateReview, evaluateBatch }; diff --git a/reputation-review-abuse-guard/readme.md b/reputation-review-abuse-guard/readme.md new file mode 100644 index 00000000..f1331fce --- /dev/null +++ b/reputation-review-abuse-guard/readme.md @@ -0,0 +1,33 @@ +# Reputation Review-Abuse Guard + +This module adds a focused moderation layer for the Community & User Reputation System. It decides whether peer-review activity should count toward reputation and leaderboard updates. + +It is intentionally separate from broad reputation scoring, contributor credit graphs, review templates, profile dashboards, badge systems, and leaderboards. This guard only handles abuse and conflict signals around review-derived reputation credit. + +## What It Checks + +- Shared-affiliation conflicts between reviewer and target author +- Reciprocal review links +- High reputation deltas from low-effort review text +- Anonymous-review identity leakage +- Endorsement-cluster farming signals +- Deterministic count, hold, and moderation actions + +## Demo + +Run: + +```bash +node reputation-review-abuse-guard/test.js +node reputation-review-abuse-guard/demo.js +node reputation-review-abuse-guard/render-video.js +``` + +Generated artifacts: + +- `reports/review-abuse-report.json` +- `reports/review-abuse-report.md` +- `reports/summary.svg` +- `reports/demo.mp4` + +All data is synthetic. The module does not call identity providers, private user profiles, live reputation ledgers, payment systems, credentials, or SCIBASE production services. diff --git a/reputation-review-abuse-guard/render-video.js b/reputation-review-abuse-guard/render-video.js new file mode 100644 index 00000000..4f3acc47 --- /dev/null +++ b/reputation-review-abuse-guard/render-video.js @@ -0,0 +1,45 @@ +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const width = 960; +const height = 540; +const ppm = path.join(reportsDir, "demo-frame.ppm"); +const mp4 = path.join(reportsDir, "demo.mp4"); + +function pixel(x, y) { + if (y < 112) return [23, 32, 51]; + if (x > 46 && x < 914 && y > 126 && y < 188) return [255, 255, 255]; + if (x > 46 && x < 914 && y > 210 && y < 272) return [255, 255, 255]; + if (x > 46 && x < 914 && y > 294 && y < 356) return [255, 255, 255]; + if (x > 46 && x < 914 && y > 378 && y < 440) return [255, 255, 255]; + if (x > 64 && x < 238 && y > 140 && y < 174) return [4, 120, 87]; + if (x > 64 && x < 238 && y > 224 && y < 258) return [180, 83, 9]; + if (x > 64 && x < 238 && y > 308 && y < 342) return [180, 83, 9]; + if (x > 64 && x < 238 && y > 392 && y < 426) return [4, 120, 87]; + return [249, 250, 251]; +} + +const header = `P6\n${width} ${height}\n255\n`; +const body = Buffer.alloc(width * height * 3); +for (let y = 0; y < height; y += 1) { + for (let x = 0; x < width; x += 1) { + const offset = (y * width + x) * 3; + const [r, g, b] = pixel(x, y); + body[offset] = r; + body[offset + 1] = g; + body[offset + 2] = b; + } +} +fs.writeFileSync(ppm, Buffer.concat([Buffer.from(header), body])); + +const result = spawnSync("ffmpeg", [ + "-y", "-loop", "1", "-framerate", "24", "-i", ppm, + "-t", "5", "-vf", "format=yuv420p", "-movflags", "+faststart", mp4 +], { stdio: "inherit" }); + +if (result.status !== 0) throw new Error("ffmpeg failed to render demo video"); +console.log(mp4); diff --git a/reputation-review-abuse-guard/reports/demo.mp4 b/reputation-review-abuse-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d54cf18cfed03fd7a4c0499a5fdc71c62a445dd7 GIT binary patch literal 7802 zcmeHMe^37CWfXZu}-QsOoKRZ{l48pPrMUm z+Towxo7;Wg_rBlveV_OJzWu)M1B4La(&}Yil|+OR5U$~|(shhQlnX2frJNEasSY6| zh;?oj>WGr~HA0VWMka)RcgkH2=pNLbdq*-Fm4Hz4H=07%0JBP~(dSHz5DaUBc?Xw4 z^F*6Bu^m>Kt`E%OSWN{gofA|otO_(>y-8ur$(+dYH1G(K9lUlDy04>U@9QIT+^nF4 zb)Y()mLcuuO8Q$a`EsnCnDcX(a3{*5IYH3WsHI!Tg766@f75(dSjo zDFFQoRZ|BXf@NB;)1byBQm5XL*bd9C01`jAn8qyx`9f>1wK&&8S_px6`mKcp22+2h zg+fplf>gPQMH!(^G@%;mcQ4Mp{>a)+gc9*AiHQ7Onx)BvM!T!tt0jQw{7gVNPW1Cbzeoo}{ z8uKzE4^%jW6OQDTW@~u`Y;mo%ck~SI` z6}cdgw`lSFr*aC2GJ82lvK#}9a!IZiIG09PNisjjN?L8u>DDy4BrnhB^X1}%KoV$A zuB24w;aPIsnkaybBx}6nQA-GhcG3)GA>h!b@(Gr63KCO8SxU$flAt}bP_J?n>CZ1A z{T53;A##+P^Al>V6R09WR_kFXyd4TlS#n7j1TRA5{T%DS8Z9uwq0pXcj#%0B!;Qb*rp1n zx?#A&IBqW&^A(0-6c-^v_QT<2L4&g9=aPgMFNPv>3xSleV!fvQRN+kMP&H1b3J6{X zFTsugCxR~83mX8t69sTEwl3C?dUtQLacH!F(rv7F2hI6N9gw|bN z^eoQC!awt_b6s05wa&7o={?)f13yeKqgMh2foap)&Xj)9S@)tZ*t=-y)zY3hCl8Hp z_*2skdDmi)HMPCe@KQ$hn_}?g#~(Hae%dmM$On^Vd%7 znf=b83&(DJ_2YIt>K_xv;3W~@EGf@;{i z_+XIOgV1YZ8e6(o9!`GeyUIq#=9$0g3hg@Z*va?wL4j;(Y&$9`$#A2*jMU4S`EMO7 zIVDV(Y4D(eN|vU4PaW4)hxSx|upcEnxul^RrT%U7hrLfM^{xN3sIjYh%h8>)T6CH0 zbK726@mo`C(bl6$5Snf~a1hb74k0wX?DZ_6ysdv<6P%Qt#N)dw11*R5ZoNg_yt30e zUbR=Ao>g^XcW2LxRUn*E7M$nb2pRmSk2ft}Gi_h@fwk;5cVp_(8*b}dlyDYACYge? zHm?oOnq)g)x$>hR89b0M`wy32%|__?c95883Np`!C5jx5Di8n18^6A|{ou4It5D){ z5CBN-1$)cfI~Jz+LOQTSl(4O>lY6lgoMkUQ@7w)R^4INwOdQ~evf!rqk7VZos6>=F zlOHhJieaTs z?+3})-pw~-LJC{y)=-eXhzcpXm4cJ*2g$HfkQxF~uyhDWuV+Pt6x~X}dG~{4Sn1xt zbUO$bRvKPhx;F^Ku+pYsK#Iqucw8FB8P^<-OT(~IJT470Um7|&iN~dQTpGp~_kS9f zGRk(|lP}%5W-Td$JP|GdBakI!!0l~tmJ|VGt^s5Oaw7nO?`(JFMiD^578!wz2!Pyk zQ;Pr+w#W$F&j854{Y(n=zrEq^d-$8{bk~f;t!z@+?OR!Os6Uu`7m$SY296Q9iUAH^ zc#A9#^%sLDBK{unz)`@FQP#X2a^@NOT|UXy)FD + + Review-Abuse Reputation Guard + Peer review credit moderation before leaderboard updates + + review-101 + count-reputation-credit + 0 finding(s), delta 8 + + review-102 + hold-reputation-credit + 4 finding(s), delta 15 + + review-103 + hold-reputation-credit + 3 finding(s), delta 11 + + review-104 + count-reputation-credit + 0 finding(s), delta 5 + diff --git a/reputation-review-abuse-guard/sample-data.js b/reputation-review-abuse-guard/sample-data.js new file mode 100644 index 00000000..40523288 --- /dev/null +++ b/reputation-review-abuse-guard/sample-data.js @@ -0,0 +1,61 @@ +const reputationBatch = { + id: "reputation-batch-2026-05-29", + generatedAt: "2026-05-29T13:02:00.000Z", + reviews: [ + { + id: "review-101", + reviewer: "ana", + targetProject: "proj-climate-7", + targetAuthor: "bao", + reviewerAffiliation: "North Lab", + authorAffiliation: "South Lab", + scoreDelta: 8, + commentWords: 142, + reciprocalReviewIds: [], + anonymousMode: false, + endorsementCluster: ["li", "mina"] + }, + { + id: "review-102", + reviewer: "chen", + targetProject: "proj-neuro-4", + targetAuthor: "dina", + reviewerAffiliation: "Vector Institute", + authorAffiliation: "Vector Institute", + scoreDelta: 15, + commentWords: 18, + reciprocalReviewIds: ["review-099"], + anonymousMode: false, + endorsementCluster: ["dina", "eli", "chen"] + }, + { + id: "review-103", + reviewer: "eli", + targetProject: "proj-math-2", + targetAuthor: "faye", + reviewerAffiliation: "Open Methods Group", + authorAffiliation: "Cedar University", + scoreDelta: 11, + commentWords: 9, + reciprocalReviewIds: [], + anonymousMode: true, + leakedIdentityText: "As your labmate Eli mentioned yesterday", + endorsementCluster: ["faye", "gita", "eli"] + }, + { + id: "review-104", + reviewer: "hugo", + targetProject: "proj-protein-8", + targetAuthor: "iris", + reviewerAffiliation: "North Lab", + authorAffiliation: "West Lab", + scoreDelta: 5, + commentWords: 64, + reciprocalReviewIds: [], + anonymousMode: false, + endorsementCluster: ["jules"] + } + ] +}; + +module.exports = { reputationBatch }; diff --git a/reputation-review-abuse-guard/test.js b/reputation-review-abuse-guard/test.js new file mode 100644 index 00000000..559c7997 --- /dev/null +++ b/reputation-review-abuse-guard/test.js @@ -0,0 +1,20 @@ +const assert = require("assert"); +const { reputationBatch } = require("./sample-data"); +const { evaluateBatch } = require("./index"); + +const report = evaluateBatch(reputationBatch); + +assert.strictEqual(report.summary.total, 4); +assert.strictEqual(report.summary["count-reputation-credit"], 2); +assert.strictEqual(report.summary["hold-reputation-credit"], 2); + +const conflict = report.reviews.find((review) => review.id === "review-102"); +assert(conflict.findings.some((finding) => finding.includes("shares affiliation"))); +assert(conflict.findings.some((finding) => finding.includes("reciprocal"))); +assert(conflict.actions.includes("suppress leaderboard update")); + +const anonymous = report.reviews.find((review) => review.id === "review-103"); +assert(anonymous.findings.some((finding) => finding.includes("identity leakage"))); +assert.strictEqual(anonymous.decision, "hold-reputation-credit"); + +console.log("reputation-review-abuse-guard tests passed");