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 00000000..d54cf18c Binary files /dev/null and b/reputation-review-abuse-guard/reports/demo.mp4 differ diff --git a/reputation-review-abuse-guard/reports/review-abuse-report.json b/reputation-review-abuse-guard/reports/review-abuse-report.json new file mode 100644 index 00000000..a8c815a5 --- /dev/null +++ b/reputation-review-abuse-guard/reports/review-abuse-report.json @@ -0,0 +1,71 @@ +{ + "batchId": "reputation-batch-2026-05-29", + "generatedAt": "2026-05-29T13:02:00.000Z", + "summary": { + "total": 4, + "findingCount": 7, + "count-reputation-credit": 2, + "hold-reputation-credit": 2 + }, + "reviews": [ + { + "id": "review-101", + "reviewer": "ana", + "targetProject": "proj-climate-7", + "decision": "count-reputation-credit", + "scoreDelta": 8, + "findings": [], + "actions": [ + "apply reputation credit", + "record review audit" + ] + }, + { + "id": "review-102", + "reviewer": "chen", + "targetProject": "proj-neuro-4", + "decision": "hold-reputation-credit", + "scoreDelta": 15, + "findings": [ + "reviewer shares affiliation with target author", + "high reputation delta from low-effort review text", + "reciprocal review link detected: review-099", + "reviewer and target author appear in same endorsement cluster" + ], + "actions": [ + "hold reputation credit", + "route to trust-and-safety review", + "suppress leaderboard update" + ] + }, + { + "id": "review-103", + "reviewer": "eli", + "targetProject": "proj-math-2", + "decision": "hold-reputation-credit", + "scoreDelta": 11, + "findings": [ + "high reputation delta from low-effort review text", + "anonymous review contains identity leakage", + "reviewer and target author appear in same endorsement cluster" + ], + "actions": [ + "hold reputation credit", + "route to trust-and-safety review", + "suppress leaderboard update" + ] + }, + { + "id": "review-104", + "reviewer": "hugo", + "targetProject": "proj-protein-8", + "decision": "count-reputation-credit", + "scoreDelta": 5, + "findings": [], + "actions": [ + "apply reputation credit", + "record review audit" + ] + } + ] +} diff --git a/reputation-review-abuse-guard/reports/review-abuse-report.md b/reputation-review-abuse-guard/reports/review-abuse-report.md new file mode 100644 index 00000000..8ec94c39 --- /dev/null +++ b/reputation-review-abuse-guard/reports/review-abuse-report.md @@ -0,0 +1,41 @@ +# Reputation Review-Abuse Guard Report + +Batch: reputation-batch-2026-05-29 +Generated: 2026-05-29T13:02:00.000Z + +## Summary + +- Reviews evaluated: 4 +- Credits counted: 2 +- Credits held: 2 +- Findings: 7 + +## Decisions + +### review-101 + +- Reviewer: ana +- Decision: count-reputation-credit +- Findings: none +- Actions: apply reputation credit; record review audit + +### review-102 + +- Reviewer: chen +- Decision: hold-reputation-credit +- Findings: reviewer shares affiliation with target author; high reputation delta from low-effort review text; reciprocal review link detected: review-099; reviewer and target author appear in same endorsement cluster +- Actions: hold reputation credit; route to trust-and-safety review; suppress leaderboard update + +### review-103 + +- Reviewer: eli +- Decision: hold-reputation-credit +- Findings: high reputation delta from low-effort review text; anonymous review contains identity leakage; reviewer and target author appear in same endorsement cluster +- Actions: hold reputation credit; route to trust-and-safety review; suppress leaderboard update + +### review-104 + +- Reviewer: hugo +- Decision: count-reputation-credit +- Findings: none +- Actions: apply reputation credit; record review audit diff --git a/reputation-review-abuse-guard/reports/summary.svg b/reputation-review-abuse-guard/reports/summary.svg new file mode 100644 index 00000000..7ddecffa --- /dev/null +++ b/reputation-review-abuse-guard/reports/summary.svg @@ -0,0 +1,21 @@ + + + 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");