Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions reviewer-workload-equity-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Reviewer Workload Equity Guard

Self-contained Community & User Reputation System slice for
`SCIBASE-AI/SCIBASE.AI#15`.

The guard evaluates pending peer-review assignments before they affect profile
reputation, badges, leaderboards, or project timelines. It checks reviewer
capacity, weekly review hours, opt-outs, leave windows, rest periods, expertise
match, early-career penalty risk, and recent review-credit concentration.

This is intentionally separate from broad reputation ledgers, endorsement rings,
leaderboard eligibility, review civility, review timeliness scoring, recusal/COI,
calibration benches, edit-history integrity, identity-leak checks, appeals,
mentorship, correction-impact, credit attestation, profile visibility, and
template-rubric validation. Its job is to stop unfair negative reputation deltas
when the reviewer was overloaded, unavailable, or mismatched before scoring.

## Run

```bash
npm run check
npm test
npm run demo
npm run demo:video
```

## Outputs

- `reports/summary.json`
- `reports/reviewer-packet.md`
- `reports/summary.svg`
- `reports/demo.webm`

All data is synthetic. The module does not call identity services, profile
systems, leaderboards, email systems, review assignment systems, or external
APIs.
173 changes: 173 additions & 0 deletions reviewer-workload-equity-guard/demo-video.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
const fs = require("fs");
const os = require("os");
const path = require("path");
const { execFileSync } = require("child_process");

const reportDir = path.join(__dirname, "reports");
const outputPath = path.join(reportDir, "demo.webm");

const chromeCandidates = [
process.env.CHROME_PATH,
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
].filter(Boolean);

function findBrowser() {
const found = chromeCandidates.find((candidate) => fs.existsSync(candidate));
if (!found) {
throw new Error("Chrome or Edge was not found. Set CHROME_PATH to generate reports/demo.webm.");
}
return found;
}

function fileUrl(filePath) {
return `file:///${filePath.replace(/\\/g, "/")}`;
}

const html = String.raw`<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Reviewer workload equity guard demo</title>
<style>
html, body { margin: 0; background: #f8fafc; }
canvas { width: 960px; height: 540px; }
pre { white-space: pre-wrap; word-break: break-all; color: #f8fafc; font-size: 1px; }
</style>
</head>
<body>
<canvas id="stage" width="960" height="540"></canvas>
<pre id="out">recording</pre>
<script>
const canvas = document.getElementById("stage");
const ctx = canvas.getContext("2d");
const out = document.getElementById("out");
const checks = [
["Capacity", "Open reviews and weekly hours are checked before reputation scoring."],
["Availability", "Opt-outs, leave windows, and rest periods suppress unfair penalties."],
["Expertise", "Mismatched assignments can be routed to mentors or reassigned."],
["Concentration", "Leaderboards pause when review credit is clustered too tightly."]
];

function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}

function draw(frame) {
const t = frame / 52;
ctx.fillStyle = "#f8fafc";
ctx.fillRect(0, 0, 960, 540);
ctx.fillStyle = "#111827";
ctx.font = "bold 34px Arial";
ctx.fillText("Reviewer Workload Equity Guard", 48, 64);
ctx.font = "19px Arial";
ctx.fillStyle = "#475569";
ctx.fillText("SCIBASE #15 reputation scoring fairness demo", 48, 98);

ctx.fillStyle = "#e5e7eb";
roundRect(48, 126, 864, 30, 8);
ctx.fill();
ctx.fillStyle = "#0f766e";
roundRect(48, 126, 864 * Math.min(1, t), 30, 8);
ctx.fill();

checks.forEach(([title, text], index) => {
const y = 194 + index * 70;
const active = Math.floor(t * 4.4) >= index;
ctx.fillStyle = active ? "#ffffff" : "#eef2f7";
ctx.strokeStyle = active ? "#0f766e" : "#cbd5e1";
ctx.lineWidth = active ? 3 : 1;
roundRect(48, y, 864, 54, 8);
ctx.fill();
ctx.stroke();
ctx.fillStyle = active ? "#0f766e" : "#64748b";
ctx.font = "bold 18px Arial";
ctx.fillText(title, 70, y + 22);
ctx.fillStyle = "#334155";
ctx.font = "16px Arial";
ctx.fillText(text, 70, y + 44);
});

ctx.fillStyle = "#64748b";
ctx.font = "15px Arial";
ctx.fillText("Synthetic data only. No profile writes, leaderboard writes, identity calls, email calls, or live review system calls.", 48, 504);
}

async function main() {
if (!window.MediaRecorder) {
out.textContent = "ERROR: MediaRecorder unavailable";
return;
}
draw(0);
const stream = canvas.captureStream(12);
const recorder = new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp8" });
const chunks = [];
recorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
chunks.push(event.data);
}
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: "video/webm" });
const reader = new FileReader();
reader.onloadend = () => {
out.textContent = reader.result;
};
reader.readAsDataURL(blob);
};
recorder.start();
let frame = 0;
const timer = setInterval(() => {
draw(frame);
frame += 1;
if (frame >= 52) {
clearInterval(timer);
recorder.stop();
stream.getTracks().forEach((track) => track.stop());
}
}, 83);
}

main();
</script>
</body>
</html>`;

fs.mkdirSync(reportDir, { recursive: true });

const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "reviewer-workload-demo-"));
const htmlPath = path.join(tempDir, "demo.html");
const profileDir = path.join(tempDir, "profile");
fs.writeFileSync(htmlPath, html, "utf8");

const stdout = execFileSync(
findBrowser(),
[
"--headless=new",
"--disable-gpu",
"--disable-dev-shm-usage",
"--autoplay-policy=no-user-gesture-required",
"--run-all-compositor-stages-before-draw",
"--virtual-time-budget=7500",
`--user-data-dir=${profileDir}`,
"--dump-dom",
fileUrl(htmlPath)
],
{ encoding: "utf8", maxBuffer: 30 * 1024 * 1024 }
);

const match = stdout.match(/data:video\/webm;base64,([A-Za-z0-9+/=]+)/);
if (!match) {
throw new Error(`Demo video generation failed. Browser output ended with: ${stdout.slice(-600)}`);
}

fs.writeFileSync(outputPath, Buffer.from(match[1], "base64"));
console.log(`Generated ${path.relative(process.cwd(), outputPath)}`);
18 changes: 18 additions & 0 deletions reviewer-workload-equity-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const fs = require("fs");
const path = require("path");
const { project } = require("./sample-data");
const { buildReviewPacket, renderMarkdownReport, renderSvgSummary } = require("./index");

const reportDir = path.join(__dirname, "reports");
fs.mkdirSync(reportDir, { recursive: true });

const packet = buildReviewPacket(project);

fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(packet, null, 2)}\n`, "utf8");
fs.writeFileSync(path.join(reportDir, "reviewer-packet.md"), renderMarkdownReport(packet), "utf8");
fs.writeFileSync(path.join(reportDir, "summary.svg"), renderSvgSummary(packet), "utf8");

console.log(`Generated reports for ${packet.guard}`);
console.log(`Decision: ${packet.decision}`);
console.log(`Score: ${packet.score}`);
console.log(`Findings: ${packet.findings.length}`);
Loading