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 00000000..bc1c8a96
Binary files /dev/null and b/reputation-probation-reinstatement-guard/reports/demo.mp4 differ
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, /