From 26a0f0e6bbface91a0c5d8e1c5dd4a8be07c33e4 Mon Sep 17 00:00:00 2001 From: AlonePenguin <187998801+AlonePenguin@users.noreply.github.com> Date: Mon, 1 Jun 2026 06:12:21 -0400 Subject: [PATCH] Add reputation probation reinstatement guard --- .../.gitignore | 1 + .../README.md | 39 ++ .../demo.js | 42 ++ .../index.js | 373 ++++++++++++++++++ .../make-demo-video.js | 127 ++++++ .../package.json | 21 + .../reports/clean-audit.json | 17 + .../reports/demo.mp4 | Bin 0 -> 16963 bytes .../reports/manifest.json | 26 ++ .../reports/risky-audit.json | 252 ++++++++++++ .../reports/risky-review.md | 43 ++ .../reports/summary.svg | 13 + .../sample-data.js | 77 ++++ .../test.js | 44 +++ 14 files changed, 1075 insertions(+) create mode 100644 reputation-probation-reinstatement-guard/.gitignore create mode 100644 reputation-probation-reinstatement-guard/README.md create mode 100644 reputation-probation-reinstatement-guard/demo.js create mode 100644 reputation-probation-reinstatement-guard/index.js create mode 100644 reputation-probation-reinstatement-guard/make-demo-video.js create mode 100644 reputation-probation-reinstatement-guard/package.json create mode 100644 reputation-probation-reinstatement-guard/reports/clean-audit.json create mode 100644 reputation-probation-reinstatement-guard/reports/demo.mp4 create mode 100644 reputation-probation-reinstatement-guard/reports/manifest.json create mode 100644 reputation-probation-reinstatement-guard/reports/risky-audit.json create mode 100644 reputation-probation-reinstatement-guard/reports/risky-review.md create mode 100644 reputation-probation-reinstatement-guard/reports/summary.svg create mode 100644 reputation-probation-reinstatement-guard/sample-data.js create mode 100644 reputation-probation-reinstatement-guard/test.js diff --git a/reputation-probation-reinstatement-guard/.gitignore b/reputation-probation-reinstatement-guard/.gitignore new file mode 100644 index 00000000..2bf074d6 --- /dev/null +++ b/reputation-probation-reinstatement-guard/.gitignore @@ -0,0 +1 @@ +reports/frames/ diff --git a/reputation-probation-reinstatement-guard/README.md b/reputation-probation-reinstatement-guard/README.md new file mode 100644 index 00000000..1590cf5c --- /dev/null +++ b/reputation-probation-reinstatement-guard/README.md @@ -0,0 +1,39 @@ +# Reputation Probation Reinstatement Guard + +Self-contained reviewer artifact for SCIBASE issue #15, focused on safe reputation re-entry after moderation probation. + +This slice is intentionally narrow. It does not duplicate broad community ledgers, endorsement rings, review calibration, badge renewal/decay, leaderboard eligibility, civility checks, correction impact, peer-review recertification, workload equity, profile credit consent, or abuse detection. It evaluates whether reputation accrual can restart after a moderation outcome is already known. + +## What It Checks + +- Minimum probation windows before profile reputation deltas resume. +- Open appeals that should block reinstatement. +- Required remediation evidence and reviewer quorum. +- Public-redacted reinstatement receipts instead of private moderation-note exposure. +- Reputation deltas accidentally applied during probation. +- Capped re-entry so badges and leaderboards do not jump immediately after reinstatement. + +## Local Verification + +```sh +npm run check +npm test +npm run demo +npm run verify-video +``` + +`npm run demo` writes reviewer artifacts to `reports/`: + +- `clean-audit.json` +- `risky-audit.json` +- `risky-review.md` +- `summary.svg` +- `manifest.json` +- `demo.mp4` + +## Requirement Mapping + +- Community reputation: protects score, badge, and leaderboard updates during probation. +- Transparency: emits redacted reinstatement decisions and remediation actions. +- Fairness: requires remediation evidence and independent moderator quorum before trust re-entry. +- Reviewer demonstration: synthetic clean/risky packets produce JSON, Markdown, SVG, and MP4 artifacts without credentials or external services. diff --git a/reputation-probation-reinstatement-guard/demo.js b/reputation-probation-reinstatement-guard/demo.js new file mode 100644 index 00000000..12444bea --- /dev/null +++ b/reputation-probation-reinstatement-guard/demo.js @@ -0,0 +1,42 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { + evaluateProbationReinstatementPacket, + renderMarkdownReport, + renderSvgSummary +} = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const clean = evaluateProbationReinstatementPacket(cleanPacket, { now: "2026-06-01T11:00:00.000Z" }); +const risky = evaluateProbationReinstatementPacket(riskyPacket, { now: "2026-06-01T11:00:00.000Z" }); +const manifest = { + module: "reputation-probation-reinstatement-guard", + issue: 15, + generatedAt: "2026-06-01T11:00:00.000Z", + scenarios: [ + { name: "clean", status: clean.status, fingerprint: clean.fingerprint, findings: clean.findings.length }, + { name: "risky", status: risky.status, fingerprint: risky.fingerprint, findings: risky.findings.length } + ], + artifacts: [ + "reports/clean-audit.json", + "reports/risky-audit.json", + "reports/risky-review.md", + "reports/summary.svg", + "reports/demo.mp4" + ] +}; + +fs.writeFileSync(path.join(reportsDir, "clean-audit.json"), `${JSON.stringify(clean, null, 2)}\n`); +fs.writeFileSync(path.join(reportsDir, "risky-audit.json"), `${JSON.stringify(risky, null, 2)}\n`); +fs.writeFileSync(path.join(reportsDir, "risky-review.md"), renderMarkdownReport(risky, riskyPacket)); +fs.writeFileSync(path.join(reportsDir, "summary.svg"), renderSvgSummary(risky)); +fs.writeFileSync(path.join(reportsDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); + +console.log(`Clean status: ${clean.status} (${clean.fingerprint})`); +console.log(`Risky status: ${risky.status} (${risky.fingerprint})`); +console.log(`Wrote reviewer artifacts to ${reportsDir}`); diff --git a/reputation-probation-reinstatement-guard/index.js b/reputation-probation-reinstatement-guard/index.js new file mode 100644 index 00000000..dcf8ecab --- /dev/null +++ b/reputation-probation-reinstatement-guard/index.js @@ -0,0 +1,373 @@ +"use strict"; + +const crypto = require("node:crypto"); + +const SEVERITY_ORDER = ["critical", "high", "warning", "info"]; + +function evaluateProbationReinstatementPacket(packet, options = {}) { + if (!isPlainObject(packet)) { + throw new TypeError("evaluateProbationReinstatementPacket expects a packet object"); + } + + const now = options.now ?? new Date().toISOString(); + const policy = isPlainObject(packet.policy) ? packet.policy : {}; + const users = asArray(packet.users); + const findings = []; + + if (users.length === 0) { + findings.push( + finding( + "PACKET_SCHEMA_MISSING_USERS", + "high", + "Probation packet has no users to inspect.", + "Reputation reinstatement decisions need user moderation records.", + "users", + "Attach user probation and reinstatement records.", + "moderation lead" + ) + ); + } + + users.forEach((user, index) => inspectUser(user, index, policy, now, findings)); + + const sortedFindings = sortFindings(findings); + const status = determineStatus(sortedFindings); + const summary = summarize(status, sortedFindings, users.length); + const reinstatementDecisions = users.map((user) => { + const userFindings = sortedFindings.filter((item) => item.userId === user.id); + return { + userId: user.id, + decision: userFindings.some((item) => ["critical", "high"].includes(item.severity)) ? "HOLD" : "ALLOW", + reasonCodes: userFindings.map((item) => item.code), + reputationAccrual: userFindings.some((item) => item.code === "PROBATION_WINDOW_ACTIVE") ? "paused" : "eligible" + }; + }); + const remediationActions = sortedFindings.map((item) => ({ + code: item.code, + userId: item.userId ?? null, + owner: item.owner, + action: item.remediation + })); + const fingerprint = crypto + .createHash("sha256") + .update( + JSON.stringify({ + policy, + users: users.map((user) => ({ + id: user.id, + probation: user.probation, + reinstatement: user.reinstatement, + reputationEvents: user.reputationEvents + })), + codes: sortedFindings.map((item) => item.code) + }) + ) + .digest("hex") + .slice(0, 16); + + return { + generatedAt: now, + status, + summary, + findingCounts: countBySeverity(sortedFindings), + findings: sortedFindings, + reinstatementDecisions, + remediationActions, + fingerprint + }; +} + +function renderMarkdownReport(result, packet) { + const lines = [ + "# Reputation Probation Reinstatement Guard", + "", + `Policy: ${packet.policy?.id ?? "unknown"}`, + `Status: ${result.status}`, + `Fingerprint: ${result.fingerprint}`, + "", + "## Summary", + "", + result.summary, + "", + "## Decisions", + "" + ]; + + result.reinstatementDecisions.forEach((decision) => { + lines.push(`- ${decision.userId}: ${decision.decision}, reputation accrual ${decision.reputationAccrual}`); + }); + + lines.push("", "## Findings", ""); + if (result.findings.length === 0) { + lines.push("- No probation or reinstatement blockers found."); + } else { + result.findings.forEach((item) => { + lines.push(`- ${item.severity.toUpperCase()} ${item.code}: ${item.message}`); + lines.push(` - Remediation: ${item.remediation}`); + }); + } + + return `${lines.join("\n")}\n`; +} + +function renderSvgSummary(result) { + const counts = result.findingCounts; + const critical = counts.critical ?? 0; + const high = counts.high ?? 0; + const warning = counts.warning ?? 0; + const ready = result.status === "READY"; + const statusColor = ready ? "#16794c" : result.status === "REVIEW" ? "#a15c00" : "#a11b32"; + const holdWidth = Math.min(320, (critical + high) * 60); + const warningWidth = Math.min(220, warning * 55); + const readyWidth = ready ? 280 : Math.max(80, 280 - holdWidth); + + return [ + ``, + ``, + ``, + `Probation reinstatement guard`, + `Status ${escapeXml(result.status)} - fingerprint ${escapeXml(result.fingerprint)}`, + ``, + ``, + ``, + `TRUST`, + `Held reinstatements: ${result.reinstatementDecisions.filter((item) => item.decision === "HOLD").length}`, + `Critical/high blockers: ${critical + high}`, + `Users checked: ${result.reinstatementDecisions.length}`, + `` + ].join("\n"); +} + +function inspectUser(user, index, policy, now, findings) { + const userId = user.id ?? `user-${index}`; + const path = `users[${index}]`; + const probation = isPlainObject(user.probation) ? user.probation : {}; + const reinstatement = isPlainObject(user.reinstatement) ? user.reinstatement : {}; + const reputationEvents = asArray(user.reputationEvents); + const nowTime = timeValue(now); + + if (!user.id) { + findings.push( + finding( + "USER_MISSING_ID", + "high", + `User at index ${index} has no stable id.`, + "Reputation moderation decisions need stable profile identifiers.", + `${path}.id`, + "Attach a stable user id before evaluating reinstatement.", + "moderation lead", + userId + ) + ); + } + + if (!probation.reasonCode || !probation.startedAt) { + findings.push( + finding( + "PROBATION_RECORD_INCOMPLETE", + "high", + `${userId} has an incomplete probation record.`, + "Probation holds should explain the moderation basis and start time.", + `${path}.probation`, + "Attach reason code, start timestamp, and moderation receipt.", + "moderation lead", + userId + ) + ); + } + + const minimumDays = Number(policy.minimumProbationDays ?? 30); + const probationEnd = probation.startedAt ? timeValue(probation.startedAt) + minimumDays * 86400000 : null; + if (probationEnd && nowTime < probationEnd) { + findings.push( + finding( + "PROBATION_WINDOW_ACTIVE", + "critical", + `${userId} is still inside the minimum probation window.`, + "Reputation accrual and badge/leaderboard re-entry should remain paused until the cooling-off period ends.", + `${path}.probation.startedAt`, + `Wait until ${new Date(probationEnd).toISOString()} before reinstatement review.`, + "moderation lead", + userId + ) + ); + } + + if (probation.appealStatus === "open" || probation.appealStatus === "pending") { + findings.push( + finding( + "OPEN_APPEAL_BLOCKS_REINSTATEMENT", + "high", + `${userId} has an unresolved appeal tied to the probation record.`, + "Profiles should not receive reputation deltas while the moderation outcome is still contested.", + `${path}.probation.appealStatus`, + "Resolve the appeal or keep reputation accrual paused.", + "appeals moderator", + userId + ) + ); + } + + if (!reinstatement.requestedAt) { + findings.push( + finding( + "REINSTATEMENT_REQUEST_MISSING", + "high", + `${userId} has no reinstatement request timestamp.`, + "The system cannot prove that a contributor asked to re-enter reputation accrual.", + `${path}.reinstatement.requestedAt`, + "Record a reinstatement request and reviewer packet.", + "community manager", + userId + ) + ); + } + + const requiredActions = new Set(asArray(policy.requiredRemediationActions).map(String)); + const completedActions = new Set(asArray(reinstatement.completedActions).map(String)); + const missingActions = [...requiredActions].filter((action) => !completedActions.has(action)); + if (missingActions.length > 0) { + findings.push( + finding( + "REMEDIATION_ACTIONS_INCOMPLETE", + "high", + `${userId} has not completed required remediation action(s): ${missingActions.join(", ")}.`, + "Reinstatement should require verifiable remediation before reputation signals resume.", + `${path}.reinstatement.completedActions`, + "Complete and attach evidence for all required remediation actions.", + "community manager", + userId + ) + ); + } + + if (asArray(reinstatement.reviewers).length < Number(policy.requiredReviewerQuorum ?? 2)) { + findings.push( + finding( + "REINSTATEMENT_REVIEW_QUORUM_MISSING", + "high", + `${userId} lacks the required reinstatement reviewer quorum.`, + "Single-reviewer reinstatement can undermine moderation consistency.", + `${path}.reinstatement.reviewers`, + "Add independent moderator approvals before re-entry.", + "moderation lead", + userId + ) + ); + } + + if (reinstatement.summaryVisibility !== "public-redacted") { + findings.push( + finding( + "REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE", + "warning", + `${userId} reinstatement summary is not configured for public-redacted visibility.`, + "Community timelines need transparency without exposing private moderation notes.", + `${path}.reinstatement.summaryVisibility`, + "Publish a redacted reinstatement receipt and keep private evidence internal.", + "community manager", + userId + ) + ); + } + + const blockedEvents = reputationEvents.filter((event) => { + const eventTime = timeValue(event.at); + return eventTime >= timeValue(probation.startedAt) && eventTime <= nowTime && event.applied === true; + }); + if (blockedEvents.length > 0) { + findings.push( + finding( + "REPUTATION_DELTA_DURING_PROBATION", + "critical", + `${userId} received ${blockedEvents.length} reputation delta(s) during probation.`, + "Probation should pause reputation accrual, badges, and leaderboard effects.", + `${path}.reputationEvents`, + "Reverse or quarantine probation-window reputation deltas before reinstatement.", + "reputation admin", + userId + ) + ); + } + + if (Number(reinstatement.scoreCapPercent ?? 100) > Number(policy.maxReentryScoreCapPercent ?? 50)) { + findings.push( + finding( + "REENTRY_SCORE_CAP_TOO_HIGH", + "warning", + `${userId} re-entry score cap is above policy.`, + "Gradual re-entry protects leaderboards and badges from immediate trust jumps.", + `${path}.reinstatement.scoreCapPercent`, + "Apply the policy score cap until the post-reinstatement review window closes.", + "reputation admin", + userId + ) + ); + } +} + +function determineStatus(findings) { + if (findings.some((item) => item.severity === "critical" || item.severity === "high")) { + return "HOLD"; + } + if (findings.some((item) => item.severity === "warning")) { + return "REVIEW"; + } + return "READY"; +} + +function summarize(status, findings, userCount) { + if (status === "READY") { + return `All ${userCount} user probation record(s) are eligible for transparent, capped reputation re-entry.`; + } + const counts = countBySeverity(findings); + return `Reputation reinstatement is ${status.toLowerCase()} with ${counts.critical ?? 0} critical, ${counts.high ?? 0} high, and ${counts.warning ?? 0} warning finding(s).`; +} + +function finding(code, severity, message, impact, path, remediation, owner = "moderation lead", userId = null) { + return { code, severity, message, impact, path, remediation, owner, userId }; +} + +function sortFindings(findings) { + return findings.sort((left, right) => { + const severityDelta = SEVERITY_ORDER.indexOf(left.severity) - SEVERITY_ORDER.indexOf(right.severity); + if (severityDelta !== 0) { + return severityDelta; + } + return left.code.localeCompare(right.code); + }); +} + +function countBySeverity(findings) { + return findings.reduce((counts, item) => { + counts[item.severity] = (counts[item.severity] ?? 0) + 1; + return counts; + }, {}); +} + +function timeValue(value) { + const parsed = new Date(value).getTime(); + return Number.isFinite(parsed) ? parsed : 0; +} + +function asArray(value) { + return Array.isArray(value) ? value : []; +} + +function isPlainObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +module.exports = { + evaluateProbationReinstatementPacket, + renderMarkdownReport, + renderSvgSummary +}; diff --git a/reputation-probation-reinstatement-guard/make-demo-video.js b/reputation-probation-reinstatement-guard/make-demo-video.js new file mode 100644 index 00000000..3f4438d7 --- /dev/null +++ b/reputation-probation-reinstatement-guard/make-demo-video.js @@ -0,0 +1,127 @@ +"use strict"; + +const { execFileSync } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); + +const WIDTH = 960; +const HEIGHT = 540; +const FONT = { + A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"], + C: ["01111", "10000", "10000", "10000", "10000", "10000", "01111"], + D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"], + E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"], + G: ["01111", "10000", "10000", "10111", "10001", "10001", "01111"], + H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"], + I: ["11111", "00100", "00100", "00100", "00100", "00100", "11111"], + L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"], + O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"], + P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"], + R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"], + S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"], + T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"], + U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"], + V: ["10001", "10001", "10001", "10001", "01010", "01010", "00100"], + X: ["10001", "01010", "00100", "00100", "00100", "01010", "10001"], + Y: ["10001", "01010", "00100", "00100", "00100", "00100", "00100"] +}; + +const reportsDir = path.join(__dirname, "reports"); +const framesDir = path.join(reportsDir, "frames"); +fs.mkdirSync(framesDir, { recursive: true }); + +for (const file of fs.readdirSync(framesDir)) { + fs.unlinkSync(path.join(framesDir, file)); +} + +const slides = [ + { label: "PROBATION", color: [161, 92, 0], fill: 0.55 }, + { label: "REINSTATE", color: [22, 121, 76], fill: 0.82 }, + { label: "TRUST CAP", color: [22, 121, 76], fill: 0.74 } +]; + +let frameIndex = 0; +for (const slide of slides) { + for (let i = 0; i < 8; i += 1) { + const progress = (i + 1) / 8; + const buffer = createFrame(slide, progress); + fs.writeFileSync(path.join(framesDir, `frame-${String(frameIndex).padStart(3, "0")}.ppm`), buffer); + frameIndex += 1; + } +} + +const output = path.join(reportsDir, "demo.mp4"); +execFileSync( + "ffmpeg", + [ + "-y", + "-framerate", + "8", + "-i", + path.join(framesDir, "frame-%03d.ppm"), + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + output + ], + { stdio: "ignore" } +); + +const stats = fs.statSync(output); +console.log(`Wrote ${output} (${stats.size} bytes)`); + +function createFrame(slide, progress) { + const pixels = Buffer.alloc(WIDTH * HEIGHT * 3); + fillRect(pixels, 0, 0, WIDTH, HEIGHT, [17, 24, 39]); + fillRect(pixels, 48, 48, 864, 444, [248, 250, 252]); + fillRect(pixels, 80, 190, 800, 88, [226, 232, 240]); + fillRect(pixels, 80, 190, Math.round(800 * slide.fill * progress), 88, slide.color); + fillRect(pixels, 80, 322, 240, 42, [226, 232, 240]); + fillRect(pixels, 344, 322, 240, 42, [226, 232, 240]); + fillRect(pixels, 608, 322, 240, 42, [226, 232, 240]); + fillRect(pixels, 80, 322, 70, 42, [161, 27, 50]); + fillRect(pixels, 344, 322, 120, 42, [161, 92, 0]); + fillRect(pixels, 608, 322, 220, 42, [22, 121, 76]); + drawText(pixels, "REPUTATION GUARD", 82, 104, 5, [17, 24, 39]); + drawText(pixels, slide.label, 108, 214, 7, [255, 255, 255]); + drawText(pixels, "SAFE REENTRY", 82, 414, 4, [51, 65, 85]); + return Buffer.concat([Buffer.from(`P6\n${WIDTH} ${HEIGHT}\n255\n`, "ascii"), pixels]); +} + +function fillRect(pixels, x, y, width, height, color) { + const x2 = Math.min(WIDTH, x + width); + const y2 = Math.min(HEIGHT, y + height); + for (let row = Math.max(0, y); row < y2; row += 1) { + for (let col = Math.max(0, x); col < x2; col += 1) { + const offset = (row * WIDTH + col) * 3; + pixels[offset] = color[0]; + pixels[offset + 1] = color[1]; + pixels[offset + 2] = color[2]; + } + } +} + +function drawText(pixels, text, x, y, scale, color) { + let cursor = x; + for (const rawChar of text) { + const char = rawChar.toUpperCase(); + if (char === " ") { + cursor += 4 * scale; + continue; + } + const glyph = FONT[char]; + if (!glyph) { + cursor += 6 * scale; + continue; + } + glyph.forEach((row, rowIndex) => { + for (let colIndex = 0; colIndex < row.length; colIndex += 1) { + if (row[colIndex] === "1") { + fillRect(pixels, cursor + colIndex * scale, y + rowIndex * scale, scale, scale, color); + } + } + }); + cursor += 6 * scale; + } +} diff --git a/reputation-probation-reinstatement-guard/package.json b/reputation-probation-reinstatement-guard/package.json new file mode 100644 index 00000000..e654ea9c --- /dev/null +++ b/reputation-probation-reinstatement-guard/package.json @@ -0,0 +1,21 @@ +{ + "name": "reputation-probation-reinstatement-guard", + "version": "1.0.0", + "private": true, + "description": "Synthetic reputation probation and reinstatement guard for community trust systems.", + "main": "index.js", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js && node --check make-demo-video.js", + "test": "node test.js", + "demo": "node demo.js && node make-demo-video.js", + "verify-video": "ffprobe -v error -show_entries stream=codec_name,width,height,duration -of default=nokey=1:noprint_wrappers=1 reports/demo.mp4" + }, + "keywords": [ + "community-reputation", + "probation", + "reinstatement", + "moderation", + "synthetic" + ], + "license": "MIT" +} diff --git a/reputation-probation-reinstatement-guard/reports/clean-audit.json b/reputation-probation-reinstatement-guard/reports/clean-audit.json new file mode 100644 index 00000000..6594fd5b --- /dev/null +++ b/reputation-probation-reinstatement-guard/reports/clean-audit.json @@ -0,0 +1,17 @@ +{ + "generatedAt": "2026-06-01T11:00:00.000Z", + "status": "READY", + "summary": "All 1 user probation record(s) are eligible for transparent, capped reputation re-entry.", + "findingCounts": {}, + "findings": [], + "reinstatementDecisions": [ + { + "userId": "researcher-alpha", + "decision": "ALLOW", + "reasonCodes": [], + "reputationAccrual": "eligible" + } + ], + "remediationActions": [], + "fingerprint": "a64ef993078090a8" +} diff --git a/reputation-probation-reinstatement-guard/reports/demo.mp4 b/reputation-probation-reinstatement-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..bc1c8a966eed50e3dc9c06b2a86b16ba87c10919 GIT binary patch literal 16963 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 literal 0 HcmV?d00001 diff --git a/reputation-probation-reinstatement-guard/reports/manifest.json b/reputation-probation-reinstatement-guard/reports/manifest.json new file mode 100644 index 00000000..d498d56e --- /dev/null +++ b/reputation-probation-reinstatement-guard/reports/manifest.json @@ -0,0 +1,26 @@ +{ + "module": "reputation-probation-reinstatement-guard", + "issue": 15, + "generatedAt": "2026-06-01T11:00:00.000Z", + "scenarios": [ + { + "name": "clean", + "status": "READY", + "fingerprint": "a64ef993078090a8", + "findings": 0 + }, + { + "name": "risky", + "status": "HOLD", + "fingerprint": "efef8995221f8246", + "findings": 13 + } + ], + "artifacts": [ + "reports/clean-audit.json", + "reports/risky-audit.json", + "reports/risky-review.md", + "reports/summary.svg", + "reports/demo.mp4" + ] +} diff --git a/reputation-probation-reinstatement-guard/reports/risky-audit.json b/reputation-probation-reinstatement-guard/reports/risky-audit.json new file mode 100644 index 00000000..cacf3a9e --- /dev/null +++ b/reputation-probation-reinstatement-guard/reports/risky-audit.json @@ -0,0 +1,252 @@ +{ + "generatedAt": "2026-06-01T11:00:00.000Z", + "status": "HOLD", + "summary": "Reputation reinstatement is hold with 2 critical, 7 high, and 4 warning finding(s).", + "findingCounts": { + "critical": 2, + "high": 7, + "warning": 4 + }, + "findings": [ + { + "code": "PROBATION_WINDOW_ACTIVE", + "severity": "critical", + "message": "researcher-beta is still inside the minimum probation window.", + "impact": "Reputation accrual and badge/leaderboard re-entry should remain paused until the cooling-off period ends.", + "path": "users[0].probation.startedAt", + "remediation": "Wait until 2026-06-19T12:00:00.000Z before reinstatement review.", + "owner": "moderation lead", + "userId": "researcher-beta" + }, + { + "code": "REPUTATION_DELTA_DURING_PROBATION", + "severity": "critical", + "message": "researcher-beta received 2 reputation delta(s) during probation.", + "impact": "Probation should pause reputation accrual, badges, and leaderboard effects.", + "path": "users[0].reputationEvents", + "remediation": "Reverse or quarantine probation-window reputation deltas before reinstatement.", + "owner": "reputation admin", + "userId": "researcher-beta" + }, + { + "code": "OPEN_APPEAL_BLOCKS_REINSTATEMENT", + "severity": "high", + "message": "researcher-beta has an unresolved appeal tied to the probation record.", + "impact": "Profiles should not receive reputation deltas while the moderation outcome is still contested.", + "path": "users[0].probation.appealStatus", + "remediation": "Resolve the appeal or keep reputation accrual paused.", + "owner": "appeals moderator", + "userId": "researcher-beta" + }, + { + "code": "PROBATION_RECORD_INCOMPLETE", + "severity": "high", + "message": "researcher-gamma has an incomplete probation record.", + "impact": "Probation holds should explain the moderation basis and start time.", + "path": "users[1].probation", + "remediation": "Attach reason code, start timestamp, and moderation receipt.", + "owner": "moderation lead", + "userId": "researcher-gamma" + }, + { + "code": "REINSTATEMENT_REQUEST_MISSING", + "severity": "high", + "message": "researcher-gamma has no reinstatement request timestamp.", + "impact": "The system cannot prove that a contributor asked to re-enter reputation accrual.", + "path": "users[1].reinstatement.requestedAt", + "remediation": "Record a reinstatement request and reviewer packet.", + "owner": "community manager", + "userId": "researcher-gamma" + }, + { + "code": "REINSTATEMENT_REVIEW_QUORUM_MISSING", + "severity": "high", + "message": "researcher-beta lacks the required reinstatement reviewer quorum.", + "impact": "Single-reviewer reinstatement can undermine moderation consistency.", + "path": "users[0].reinstatement.reviewers", + "remediation": "Add independent moderator approvals before re-entry.", + "owner": "moderation lead", + "userId": "researcher-beta" + }, + { + "code": "REINSTATEMENT_REVIEW_QUORUM_MISSING", + "severity": "high", + "message": "researcher-gamma lacks the required reinstatement reviewer quorum.", + "impact": "Single-reviewer reinstatement can undermine moderation consistency.", + "path": "users[1].reinstatement.reviewers", + "remediation": "Add independent moderator approvals before re-entry.", + "owner": "moderation lead", + "userId": "researcher-gamma" + }, + { + "code": "REMEDIATION_ACTIONS_INCOMPLETE", + "severity": "high", + "message": "researcher-beta has not completed required remediation action(s): updated-profile-attestation.", + "impact": "Reinstatement should require verifiable remediation before reputation signals resume.", + "path": "users[0].reinstatement.completedActions", + "remediation": "Complete and attach evidence for all required remediation actions.", + "owner": "community manager", + "userId": "researcher-beta" + }, + { + "code": "REMEDIATION_ACTIONS_INCOMPLETE", + "severity": "high", + "message": "researcher-gamma has not completed required remediation action(s): moderation-acknowledgement, updated-profile-attestation.", + "impact": "Reinstatement should require verifiable remediation before reputation signals resume.", + "path": "users[1].reinstatement.completedActions", + "remediation": "Complete and attach evidence for all required remediation actions.", + "owner": "community manager", + "userId": "researcher-gamma" + }, + { + "code": "REENTRY_SCORE_CAP_TOO_HIGH", + "severity": "warning", + "message": "researcher-beta re-entry score cap is above policy.", + "impact": "Gradual re-entry protects leaderboards and badges from immediate trust jumps.", + "path": "users[0].reinstatement.scoreCapPercent", + "remediation": "Apply the policy score cap until the post-reinstatement review window closes.", + "owner": "reputation admin", + "userId": "researcher-beta" + }, + { + "code": "REENTRY_SCORE_CAP_TOO_HIGH", + "severity": "warning", + "message": "researcher-gamma re-entry score cap is above policy.", + "impact": "Gradual re-entry protects leaderboards and badges from immediate trust jumps.", + "path": "users[1].reinstatement.scoreCapPercent", + "remediation": "Apply the policy score cap until the post-reinstatement review window closes.", + "owner": "reputation admin", + "userId": "researcher-gamma" + }, + { + "code": "REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE", + "severity": "warning", + "message": "researcher-beta reinstatement summary is not configured for public-redacted visibility.", + "impact": "Community timelines need transparency without exposing private moderation notes.", + "path": "users[0].reinstatement.summaryVisibility", + "remediation": "Publish a redacted reinstatement receipt and keep private evidence internal.", + "owner": "community manager", + "userId": "researcher-beta" + }, + { + "code": "REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE", + "severity": "warning", + "message": "researcher-gamma reinstatement summary is not configured for public-redacted visibility.", + "impact": "Community timelines need transparency without exposing private moderation notes.", + "path": "users[1].reinstatement.summaryVisibility", + "remediation": "Publish a redacted reinstatement receipt and keep private evidence internal.", + "owner": "community manager", + "userId": "researcher-gamma" + } + ], + "reinstatementDecisions": [ + { + "userId": "researcher-beta", + "decision": "HOLD", + "reasonCodes": [ + "PROBATION_WINDOW_ACTIVE", + "REPUTATION_DELTA_DURING_PROBATION", + "OPEN_APPEAL_BLOCKS_REINSTATEMENT", + "REINSTATEMENT_REVIEW_QUORUM_MISSING", + "REMEDIATION_ACTIONS_INCOMPLETE", + "REENTRY_SCORE_CAP_TOO_HIGH", + "REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE" + ], + "reputationAccrual": "paused" + }, + { + "userId": "researcher-gamma", + "decision": "HOLD", + "reasonCodes": [ + "PROBATION_RECORD_INCOMPLETE", + "REINSTATEMENT_REQUEST_MISSING", + "REINSTATEMENT_REVIEW_QUORUM_MISSING", + "REMEDIATION_ACTIONS_INCOMPLETE", + "REENTRY_SCORE_CAP_TOO_HIGH", + "REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE" + ], + "reputationAccrual": "eligible" + } + ], + "remediationActions": [ + { + "code": "PROBATION_WINDOW_ACTIVE", + "userId": "researcher-beta", + "owner": "moderation lead", + "action": "Wait until 2026-06-19T12:00:00.000Z before reinstatement review." + }, + { + "code": "REPUTATION_DELTA_DURING_PROBATION", + "userId": "researcher-beta", + "owner": "reputation admin", + "action": "Reverse or quarantine probation-window reputation deltas before reinstatement." + }, + { + "code": "OPEN_APPEAL_BLOCKS_REINSTATEMENT", + "userId": "researcher-beta", + "owner": "appeals moderator", + "action": "Resolve the appeal or keep reputation accrual paused." + }, + { + "code": "PROBATION_RECORD_INCOMPLETE", + "userId": "researcher-gamma", + "owner": "moderation lead", + "action": "Attach reason code, start timestamp, and moderation receipt." + }, + { + "code": "REINSTATEMENT_REQUEST_MISSING", + "userId": "researcher-gamma", + "owner": "community manager", + "action": "Record a reinstatement request and reviewer packet." + }, + { + "code": "REINSTATEMENT_REVIEW_QUORUM_MISSING", + "userId": "researcher-beta", + "owner": "moderation lead", + "action": "Add independent moderator approvals before re-entry." + }, + { + "code": "REINSTATEMENT_REVIEW_QUORUM_MISSING", + "userId": "researcher-gamma", + "owner": "moderation lead", + "action": "Add independent moderator approvals before re-entry." + }, + { + "code": "REMEDIATION_ACTIONS_INCOMPLETE", + "userId": "researcher-beta", + "owner": "community manager", + "action": "Complete and attach evidence for all required remediation actions." + }, + { + "code": "REMEDIATION_ACTIONS_INCOMPLETE", + "userId": "researcher-gamma", + "owner": "community manager", + "action": "Complete and attach evidence for all required remediation actions." + }, + { + "code": "REENTRY_SCORE_CAP_TOO_HIGH", + "userId": "researcher-beta", + "owner": "reputation admin", + "action": "Apply the policy score cap until the post-reinstatement review window closes." + }, + { + "code": "REENTRY_SCORE_CAP_TOO_HIGH", + "userId": "researcher-gamma", + "owner": "reputation admin", + "action": "Apply the policy score cap until the post-reinstatement review window closes." + }, + { + "code": "REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE", + "userId": "researcher-beta", + "owner": "community manager", + "action": "Publish a redacted reinstatement receipt and keep private evidence internal." + }, + { + "code": "REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE", + "userId": "researcher-gamma", + "owner": "community manager", + "action": "Publish a redacted reinstatement receipt and keep private evidence internal." + } + ], + "fingerprint": "efef8995221f8246" +} diff --git a/reputation-probation-reinstatement-guard/reports/risky-review.md b/reputation-probation-reinstatement-guard/reports/risky-review.md new file mode 100644 index 00000000..d17a7125 --- /dev/null +++ b/reputation-probation-reinstatement-guard/reports/risky-review.md @@ -0,0 +1,43 @@ +# Reputation Probation Reinstatement Guard + +Policy: community-reputation-probation-2026 +Status: HOLD +Fingerprint: efef8995221f8246 + +## Summary + +Reputation reinstatement is hold with 2 critical, 7 high, and 4 warning finding(s). + +## Decisions + +- researcher-beta: HOLD, reputation accrual paused +- researcher-gamma: HOLD, reputation accrual eligible + +## Findings + +- CRITICAL PROBATION_WINDOW_ACTIVE: researcher-beta is still inside the minimum probation window. + - Remediation: Wait until 2026-06-19T12:00:00.000Z before reinstatement review. +- CRITICAL REPUTATION_DELTA_DURING_PROBATION: researcher-beta received 2 reputation delta(s) during probation. + - Remediation: Reverse or quarantine probation-window reputation deltas before reinstatement. +- HIGH OPEN_APPEAL_BLOCKS_REINSTATEMENT: researcher-beta has an unresolved appeal tied to the probation record. + - Remediation: Resolve the appeal or keep reputation accrual paused. +- HIGH PROBATION_RECORD_INCOMPLETE: researcher-gamma has an incomplete probation record. + - Remediation: Attach reason code, start timestamp, and moderation receipt. +- HIGH REINSTATEMENT_REQUEST_MISSING: researcher-gamma has no reinstatement request timestamp. + - Remediation: Record a reinstatement request and reviewer packet. +- HIGH REINSTATEMENT_REVIEW_QUORUM_MISSING: researcher-beta lacks the required reinstatement reviewer quorum. + - Remediation: Add independent moderator approvals before re-entry. +- HIGH REINSTATEMENT_REVIEW_QUORUM_MISSING: researcher-gamma lacks the required reinstatement reviewer quorum. + - Remediation: Add independent moderator approvals before re-entry. +- HIGH REMEDIATION_ACTIONS_INCOMPLETE: researcher-beta has not completed required remediation action(s): updated-profile-attestation. + - Remediation: Complete and attach evidence for all required remediation actions. +- HIGH REMEDIATION_ACTIONS_INCOMPLETE: researcher-gamma has not completed required remediation action(s): moderation-acknowledgement, updated-profile-attestation. + - Remediation: Complete and attach evidence for all required remediation actions. +- WARNING REENTRY_SCORE_CAP_TOO_HIGH: researcher-beta re-entry score cap is above policy. + - Remediation: Apply the policy score cap until the post-reinstatement review window closes. +- WARNING REENTRY_SCORE_CAP_TOO_HIGH: researcher-gamma re-entry score cap is above policy. + - Remediation: Apply the policy score cap until the post-reinstatement review window closes. +- WARNING REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE: researcher-beta reinstatement summary is not configured for public-redacted visibility. + - Remediation: Publish a redacted reinstatement receipt and keep private evidence internal. +- WARNING REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE: researcher-gamma reinstatement summary is not configured for public-redacted visibility. + - Remediation: Publish a redacted reinstatement receipt and keep private evidence internal. diff --git a/reputation-probation-reinstatement-guard/reports/summary.svg b/reputation-probation-reinstatement-guard/reports/summary.svg new file mode 100644 index 00000000..d37e8d3a --- /dev/null +++ b/reputation-probation-reinstatement-guard/reports/summary.svg @@ -0,0 +1,13 @@ + + + +Probation reinstatement guard +Status HOLD - fingerprint efef8995221f8246 + + + +TRUST +Held reinstatements: 2 +Critical/high blockers: 9 +Users checked: 2 + \ No newline at end of file diff --git a/reputation-probation-reinstatement-guard/sample-data.js b/reputation-probation-reinstatement-guard/sample-data.js new file mode 100644 index 00000000..096f1377 --- /dev/null +++ b/reputation-probation-reinstatement-guard/sample-data.js @@ -0,0 +1,77 @@ +"use strict"; + +const cleanPacket = { + policy: { + id: "community-reputation-probation-2026", + minimumProbationDays: 30, + requiredRemediationActions: ["moderation-acknowledgement", "updated-profile-attestation"], + requiredReviewerQuorum: 2, + maxReentryScoreCapPercent: 50 + }, + users: [ + { + id: "researcher-alpha", + probation: { + reasonCode: "unsupported-review-claim", + startedAt: "2026-04-20T12:00:00.000Z", + appealStatus: "resolved", + receiptHash: "sha256:probation-alpha" + }, + reinstatement: { + requestedAt: "2026-05-25T12:00:00.000Z", + completedActions: ["moderation-acknowledgement", "updated-profile-attestation"], + reviewers: ["moderator-a", "moderator-b"], + summaryVisibility: "public-redacted", + scoreCapPercent: 40 + }, + reputationEvents: [ + { at: "2026-04-18T12:00:00.000Z", delta: 4, applied: true, source: "accepted-review" }, + { at: "2026-05-26T12:00:00.000Z", delta: 0, applied: false, source: "probation-review" } + ] + } + ] +}; + +const riskyPacket = { + policy: cleanPacket.policy, + users: [ + { + id: "researcher-beta", + probation: { + reasonCode: "review-abuse", + startedAt: "2026-05-20T12:00:00.000Z", + appealStatus: "open", + receiptHash: "sha256:probation-beta" + }, + reinstatement: { + requestedAt: "2026-05-25T12:00:00.000Z", + completedActions: ["moderation-acknowledgement"], + reviewers: ["moderator-a"], + summaryVisibility: "private", + scoreCapPercent: 80 + }, + reputationEvents: [ + { at: "2026-05-22T12:00:00.000Z", delta: 6, applied: true, source: "reviewer-badge" }, + { at: "2026-05-23T12:00:00.000Z", delta: 2, applied: true, source: "leaderboard-credit" } + ] + }, + { + id: "researcher-gamma", + probation: { + startedAt: "2026-04-10T12:00:00.000Z", + appealStatus: "resolved" + }, + reinstatement: { + completedActions: [], + reviewers: [], + summaryVisibility: "public" + }, + reputationEvents: [] + } + ] +}; + +module.exports = { + cleanPacket, + riskyPacket +}; diff --git a/reputation-probation-reinstatement-guard/test.js b/reputation-probation-reinstatement-guard/test.js new file mode 100644 index 00000000..a78303d4 --- /dev/null +++ b/reputation-probation-reinstatement-guard/test.js @@ -0,0 +1,44 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const { + evaluateProbationReinstatementPacket, + renderMarkdownReport, + renderSvgSummary +} = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const clean = evaluateProbationReinstatementPacket(cleanPacket, { now: "2026-06-01T11:00:00.000Z" }); +assert.equal(clean.status, "READY"); +assert.equal(clean.findings.length, 0); +assert.equal(clean.reinstatementDecisions[0].decision, "ALLOW"); + +const risky = evaluateProbationReinstatementPacket(riskyPacket, { now: "2026-06-01T11:00:00.000Z" }); +const codes = new Set(risky.findings.map((finding) => finding.code)); +assert.equal(risky.status, "HOLD"); +assert.ok(codes.has("PROBATION_WINDOW_ACTIVE")); +assert.ok(codes.has("OPEN_APPEAL_BLOCKS_REINSTATEMENT")); +assert.ok(codes.has("REMEDIATION_ACTIONS_INCOMPLETE")); +assert.ok(codes.has("REINSTATEMENT_REVIEW_QUORUM_MISSING")); +assert.ok(codes.has("REINSTATEMENT_SUMMARY_VISIBILITY_UNSAFE")); +assert.ok(codes.has("REPUTATION_DELTA_DURING_PROBATION")); +assert.ok(codes.has("REENTRY_SCORE_CAP_TOO_HIGH")); +assert.ok(codes.has("PROBATION_RECORD_INCOMPLETE")); +assert.ok(codes.has("REINSTATEMENT_REQUEST_MISSING")); +assert.equal(risky.reinstatementDecisions.filter((decision) => decision.decision === "HOLD").length, 2); + +const repeatedRisky = evaluateProbationReinstatementPacket(riskyPacket, { now: "2026-06-01T11:05:00.000Z" }); +assert.equal(risky.fingerprint, repeatedRisky.fingerprint); + +const markdown = renderMarkdownReport(risky, riskyPacket); +assert.match(markdown, /Reputation Probation Reinstatement Guard/); +assert.match(markdown, /PROBATION_WINDOW_ACTIVE/); + +const svg = renderSvgSummary(risky); +assert.match(svg, / evaluateProbationReinstatementPacket(null), /expects a packet object/); + +console.log("All reputation probation reinstatement guard tests passed.");