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 [
+ ``
+ ].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 @@
+
\ 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, /