Skip to content
22 changes: 22 additions & 0 deletions scientific-bounty-reviewer-capacity-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Scientific bounty reviewer capacity guard

This module checks whether a scientific bounty has enough qualified, available reviewers before a review panel is assigned.

It focuses on one slice of the bounty workflow: reviewer capacity. It does not handle payouts, arbitration, escrow, IP transfer, or export control.

## What it checks

- enough eligible reviewers
- required expertise coverage
- reviewer conflicts
- overloaded reviewers
- stale reviewer training

## Run it

```bash
node scientific-bounty-reviewer-capacity-guard/test.js
node scientific-bounty-reviewer-capacity-guard/demo.js
```

The demo writes a JSON result and Markdown report to `artifacts/`.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Scientific bounty reviewer capacity report

## BIO-42: Biomarker scoring challenge
Decision: ASSIGN_PANEL
Eligible reviewers: r-ada, r-lin
Next step: assign the listed reviewers and open the review window

## MAT-11: Low heat cement materials challenge
Decision: HOLD_ASSIGNMENT
Eligible reviewers: r-ivy
Next step: hold assignment until coverage and capacity are fixed
Blockers:
- not enough eligible reviewers
- conflicts: r-noah
- overloaded: r-mira

## QNT-7: Quantum noise reduction challenge
Decision: HOLD_ASSIGNMENT
Eligible reviewers: r-june
Next step: hold assignment until coverage and capacity are fixed
Blockers:
- not enough eligible reviewers
- missing expertise: quantum, signal-processing
- stale training: r-sam
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[
{
"challengeId": "BIO-42",
"title": "Biomarker scoring challenge",
"decision": "ASSIGN_PANEL",
"eligibleReviewers": [
"r-ada",
"r-lin"
],
"missingExpertise": [],
"blockers": [],
"nextStep": "assign the listed reviewers and open the review window"
},
{
"challengeId": "MAT-11",
"title": "Low heat cement materials challenge",
"decision": "HOLD_ASSIGNMENT",
"eligibleReviewers": [
"r-ivy"
],
"missingExpertise": [],
"blockers": [
"not enough eligible reviewers",
"conflicts: r-noah",
"overloaded: r-mira"
],
"nextStep": "hold assignment until coverage and capacity are fixed"
},
{
"challengeId": "QNT-7",
"title": "Quantum noise reduction challenge",
"decision": "HOLD_ASSIGNMENT",
"eligibleReviewers": [
"r-june"
],
"missingExpertise": [
"quantum",
"signal-processing"
],
"blockers": [
"not enough eligible reviewers",
"missing expertise: quantum, signal-processing",
"stale training: r-sam"
],
"nextStep": "hold assignment until coverage and capacity are fixed"
}
]
13 changes: 13 additions & 0 deletions scientific-bounty-reviewer-capacity-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const fs = require("fs");
const path = require("path");
const challenges = require("./sample-data.json");
const { evaluatePortfolio, renderMarkdownReport } = require("./index");

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

const results = evaluatePortfolio(challenges);
fs.writeFileSync(path.join(artifactsDir, "reviewer-capacity-results.json"), JSON.stringify(results, null, 2));
fs.writeFileSync(path.join(artifactsDir, "reviewer-capacity-report.md"), renderMarkdownReport(results));

console.log(renderMarkdownReport(results));
81 changes: 81 additions & 0 deletions scientific-bounty-reviewer-capacity-guard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const REQUIRED_REVIEWERS = 2;

function scoreChallenge(challenge) {
const requiredExpertise = new Set(challenge.requiredExpertise || []);
const reviewers = challenge.reviewers || [];

const eligible = reviewers.filter((reviewer) => {
const hasConflict = (reviewer.conflicts || []).includes(challenge.id);
const isOverloaded = reviewer.activeReviews >= reviewer.maxActiveReviews;
const coversExpertise = (reviewer.expertise || []).some((item) => requiredExpertise.has(item));
return !hasConflict && !isOverloaded && reviewer.trainingCurrent && reviewer.available;
});

const coveredExpertise = new Set();
for (const reviewer of eligible) {
for (const item of reviewer.expertise || []) {
if (requiredExpertise.has(item)) coveredExpertise.add(item);
}
}

const missingExpertise = [...requiredExpertise].filter((item) => !coveredExpertise.has(item));
const overloaded = reviewers.filter((reviewer) => reviewer.activeReviews >= reviewer.maxActiveReviews);
const conflicted = reviewers.filter((reviewer) => (reviewer.conflicts || []).includes(challenge.id));
const staleTraining = reviewers.filter((reviewer) => !reviewer.trainingCurrent);

const blockers = [];
if (eligible.length < REQUIRED_REVIEWERS) blockers.push("not enough eligible reviewers");
if (missingExpertise.length) blockers.push(`missing expertise: ${missingExpertise.join(", ")}`);
if (conflicted.length) blockers.push(`conflicts: ${conflicted.map((r) => r.id).join(", ")}`);
if (overloaded.length) blockers.push(`overloaded: ${overloaded.map((r) => r.id).join(", ")}`);
if (staleTraining.length) blockers.push(`stale training: ${staleTraining.map((r) => r.id).join(", ")}`);

let decision = "ASSIGN_PANEL";
if (eligible.length < REQUIRED_REVIEWERS || missingExpertise.length) {
decision = "HOLD_ASSIGNMENT";
} else if (conflicted.length || overloaded.length || staleTraining.length) {
decision = "REBALANCE_PANEL";
}

return {
challengeId: challenge.id,
title: challenge.title,
decision,
eligibleReviewers: eligible.map((reviewer) => reviewer.id),
missingExpertise,
blockers,
nextStep: nextStepFor(decision),
};
}

function nextStepFor(decision) {
if (decision === "ASSIGN_PANEL") return "assign the listed reviewers and open the review window";
if (decision === "REBALANCE_PANEL") return "rebalance reviewer load or replace conflicted reviewers";
return "hold assignment until coverage and capacity are fixed";
}

function evaluatePortfolio(challenges) {
return challenges.map(scoreChallenge);
}

function renderMarkdownReport(results) {
const lines = ["# Scientific bounty reviewer capacity report", ""];
for (const result of results) {
lines.push(`## ${result.challengeId}: ${result.title}`);
lines.push(`Decision: ${result.decision}`);
lines.push(`Eligible reviewers: ${result.eligibleReviewers.join(", ") || "none"}`);
lines.push(`Next step: ${result.nextStep}`);
if (result.blockers.length) {
lines.push("Blockers:");
for (const blocker of result.blockers) lines.push(`- ${blocker}`);
}
lines.push("");
}
return lines.join("\n");
}

module.exports = {
evaluatePortfolio,
renderMarkdownReport,
scoreChallenge,
};
86 changes: 86 additions & 0 deletions scientific-bounty-reviewer-capacity-guard/sample-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
[
{
"id": "BIO-42",
"title": "Biomarker scoring challenge",
"requiredExpertise": ["bioinformatics", "statistics"],
"reviewers": [
{
"id": "r-ada",
"expertise": ["bioinformatics"],
"activeReviews": 1,
"maxActiveReviews": 4,
"conflicts": [],
"trainingCurrent": true,
"available": true
},
{
"id": "r-lin",
"expertise": ["statistics", "ml"],
"activeReviews": 2,
"maxActiveReviews": 4,
"conflicts": [],
"trainingCurrent": true,
"available": true
}
]
},
{
"id": "MAT-11",
"title": "Low heat cement materials challenge",
"requiredExpertise": ["materials", "climate"],
"reviewers": [
{
"id": "r-mira",
"expertise": ["materials"],
"activeReviews": 4,
"maxActiveReviews": 4,
"conflicts": [],
"trainingCurrent": true,
"available": true
},
{
"id": "r-noah",
"expertise": ["climate"],
"activeReviews": 1,
"maxActiveReviews": 3,
"conflicts": ["MAT-11"],
"trainingCurrent": true,
"available": true
},
{
"id": "r-ivy",
"expertise": ["materials", "climate"],
"activeReviews": 1,
"maxActiveReviews": 3,
"conflicts": [],
"trainingCurrent": true,
"available": true
}
]
},
{
"id": "QNT-7",
"title": "Quantum noise reduction challenge",
"requiredExpertise": ["quantum", "signal-processing"],
"reviewers": [
{
"id": "r-sam",
"expertise": ["quantum"],
"activeReviews": 2,
"maxActiveReviews": 3,
"conflicts": [],
"trainingCurrent": false,
"available": true
},
{
"id": "r-june",
"expertise": ["statistics"],
"activeReviews": 0,
"maxActiveReviews": 2,
"conflicts": [],
"trainingCurrent": true,
"available": true
}
]
}
]
18 changes: 18 additions & 0 deletions scientific-bounty-reviewer-capacity-guard/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const assert = require("assert");
const challenges = require("./sample-data.json");
const { evaluatePortfolio } = require("./index");

const results = evaluatePortfolio(challenges);
const byId = Object.fromEntries(results.map((result) => [result.challengeId, result]));

assert.strictEqual(byId["BIO-42"].decision, "ASSIGN_PANEL");
assert.deepStrictEqual(byId["BIO-42"].eligibleReviewers, ["r-ada", "r-lin"]);

assert.strictEqual(byId["MAT-11"].decision, "HOLD_ASSIGNMENT");
assert.ok(byId["MAT-11"].blockers.some((blocker) => blocker.includes("conflicts")));
assert.ok(byId["MAT-11"].blockers.some((blocker) => blocker.includes("overloaded")));

assert.strictEqual(byId["QNT-7"].decision, "HOLD_ASSIGNMENT");
assert.deepStrictEqual(byId["QNT-7"].missingExpertise, ["quantum", "signal-processing"]);

console.log("scientific bounty reviewer capacity guard tests passed");