diff --git a/scientific-bounty-escrow-readiness-guard/README.md b/scientific-bounty-escrow-readiness-guard/README.md new file mode 100644 index 00000000..b4a1f9a2 --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/README.md @@ -0,0 +1,34 @@ +# Scientific Bounty Escrow Readiness Guard + +Self-contained reviewer artifact for the Scientific Bounty System bounty. + +This module checks whether a scientific challenge has enough funded escrow +coverage before teams are allowed to invest time in submissions. It focuses on +funding readiness, not review scoring, duplicate detection, scope control, or +final payout routing. + +## Scope + +- Validate total prize amount, milestone allocations, sponsor deposit, platform + reserve, honorable mention budget, cancellation/refund window, and team split + coverage. +- Produce deterministic publish/hold/remediation decisions before submissions + open. +- Use synthetic data only. No payment rails, bank accounts, wallets, private + sponsor data, off-ramp claims, exchange claims, or external APIs. + +## Commands + +```bash +node scientific-bounty-escrow-readiness-guard/test.js +node scientific-bounty-escrow-readiness-guard/demo.js +node scientific-bounty-escrow-readiness-guard/render-video.js +node --check scientific-bounty-escrow-readiness-guard/index.js +node --check scientific-bounty-escrow-readiness-guard/sample-data.js +node --check scientific-bounty-escrow-readiness-guard/test.js +node --check scientific-bounty-escrow-readiness-guard/demo.js +node --check scientific-bounty-escrow-readiness-guard/render-video.js +``` + +The demo writes JSON, Markdown, SVG, and MP4 reviewer artifacts under +`scientific-bounty-escrow-readiness-guard/reports/`. diff --git a/scientific-bounty-escrow-readiness-guard/demo.js b/scientific-bounty-escrow-readiness-guard/demo.js new file mode 100644 index 00000000..27e1b8d5 --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/demo.js @@ -0,0 +1,30 @@ +const fs = require("fs"); +const path = require("path"); +const { + evaluateEscrowReadinessBatch, + renderMarkdownReport, + renderSvgReport, +} = require("./index"); +const { challenges } = require("./sample-data"); + +const reportDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportDir, { recursive: true }); + +const report = evaluateEscrowReadinessBatch(challenges); + +fs.writeFileSync( + path.join(reportDir, "escrow-readiness-review.json"), + `${JSON.stringify(report, null, 2)}\n` +); +fs.writeFileSync( + path.join(reportDir, "escrow-readiness-review.md"), + `${renderMarkdownReport(report)}\n` +); +fs.writeFileSync( + path.join(reportDir, "escrow-readiness-review.svg"), + renderSvgReport(report) +); + +console.log( + `wrote ${report.summary.total} escrow readiness decisions to ${path.relative(process.cwd(), reportDir)}` +); diff --git a/scientific-bounty-escrow-readiness-guard/index.js b/scientific-bounty-escrow-readiness-guard/index.js new file mode 100644 index 00000000..a0a31eb6 --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/index.js @@ -0,0 +1,273 @@ +const crypto = require("crypto"); + +function money(value) { + return Math.round(Number(value || 0) * 100) / 100; +} + +function sum(values) { + return money(values.reduce((total, value) => total + Number(value || 0), 0)); +} + +function requiredCoverage(challenge) { + return money( + challenge.prizeAmount + + challenge.platformReserve + + challenge.honorableMentionBudget + + challenge.cancellationRefundHold + ); +} + +function milestoneTotal(challenge) { + return money(sum((challenge.milestones || []).map((item) => item.percent))); +} + +function validateMilestoneSchedule(challenge) { + const blockers = []; + const warnings = []; + const milestones = challenge.milestones || []; + + const total = milestoneTotal(challenge); + if (Math.abs(total - 1) > 0.0001) { + blockers.push(`milestone percentages sum to ${total}, expected 1`); + } + + let previousDueDay = 0; + for (const milestone of milestones) { + if (!milestone.id) { + blockers.push("milestone is missing an id"); + } + if (milestone.percent <= 0) { + blockers.push(`milestone ${milestone.id || "unknown"} has non-positive percent`); + } + if (milestone.dueDay <= previousDueDay) { + blockers.push(`milestone ${milestone.id || "unknown"} is not in due-day order`); + } + previousDueDay = milestone.dueDay; + } + + const finalDue = milestones.length > 0 ? milestones[milestones.length - 1].dueDay : 0; + if (finalDue < challenge.minSubmissionWindowDays) { + warnings.push( + `final milestone day ${finalDue} is shorter than submission window ${challenge.minSubmissionWindowDays}` + ); + } + + return { blockers, warnings }; +} + +function validateTeamSplits(challenge) { + const blockers = []; + const warnings = []; + for (const split of challenge.teamSplits || []) { + const impliedMinimum = split.maxMembers * split.minMemberSharePercent; + if (impliedMinimum > 100) { + blockers.push( + `team ${split.teamId} requires ${impliedMinimum}% minimum member shares` + ); + } else if (split.minMemberSharePercent < 5) { + warnings.push(`team ${split.teamId} permits very small member shares`); + } + } + return { blockers, warnings }; +} + +function evaluateEscrowReadiness(challenge) { + const blockers = []; + const warnings = []; + const evidence = []; + const actions = []; + + const required = requiredCoverage(challenge); + const deposit = money(challenge.sponsorDeposit); + const deficit = money(Math.max(0, required - deposit)); + const surplus = money(Math.max(0, deposit - required)); + + evidence.push(`required coverage ${required} ${challenge.currency}`); + evidence.push(`sponsor deposit ${deposit} ${challenge.currency}`); + + if (deposit < required) { + blockers.push(`deposit is short by ${deficit} ${challenge.currency}`); + actions.push("hold challenge before submissions open"); + actions.push("request sponsor top-up or lower published awards"); + } else { + evidence.push(`funding surplus ${surplus} ${challenge.currency}`); + } + + if (challenge.refundLockedUntilDaysAfterOpen < 14) { + blockers.push( + `refund lock ${challenge.refundLockedUntilDaysAfterOpen} days is shorter than 14 day minimum` + ); + actions.push("extend refund lock before solver work starts"); + } + + if (challenge.honorableMentionBudget > challenge.prizeAmount * 0.25) { + warnings.push("honorable mention budget exceeds 25% of main prize"); + } + + const milestoneCheck = validateMilestoneSchedule(challenge); + blockers.push(...milestoneCheck.blockers); + warnings.push(...milestoneCheck.warnings); + + const teamCheck = validateTeamSplits(challenge); + blockers.push(...teamCheck.blockers); + warnings.push(...teamCheck.warnings); + + if (blockers.length === 0) { + actions.push("publish funding-ready challenge"); + actions.push("freeze escrow coverage snapshot for reviewer audit"); + } + + const decision = + blockers.length > 0 + ? "hold" + : warnings.length > 0 + ? "publish_with_notice" + : "publish"; + + const result = { + challengeId: challenge.id, + title: challenge.title, + sponsor: challenge.sponsor, + currency: challenge.currency, + decision, + requiredCoverage: required, + sponsorDeposit: deposit, + surplus, + deficit, + evidence, + blockers, + warnings, + actions: [...new Set(actions)], + }; + + return { + ...result, + auditDigest: digest(result), + }; +} + +function evaluateEscrowReadinessBatch(challenges) { + const decisions = challenges.map(evaluateEscrowReadiness); + const summary = decisions.reduce( + (acc, item) => { + acc.total += 1; + acc[item.decision] += 1; + acc.totalRequiredCoverage = money( + acc.totalRequiredCoverage + item.requiredCoverage + ); + acc.totalDeposits = money(acc.totalDeposits + item.sponsorDeposit); + acc.totalDeficit = money(acc.totalDeficit + item.deficit); + return acc; + }, + { + total: 0, + publish: 0, + publish_with_notice: 0, + hold: 0, + totalRequiredCoverage: 0, + totalDeposits: 0, + totalDeficit: 0, + } + ); + + return { + generatedAt: new Date().toISOString(), + summary, + decisions, + }; +} + +function digest(value) { + return crypto + .createHash("sha256") + .update(JSON.stringify(value)) + .digest("hex"); +} + +function renderMarkdownReport(report) { + const lines = [ + "# Scientific Bounty Escrow Readiness Report", + "", + `Generated: ${report.generatedAt}`, + "", + "## Summary", + "", + `- Challenges checked: ${report.summary.total}`, + `- Publish: ${report.summary.publish}`, + `- Publish with notice: ${report.summary.publish_with_notice}`, + `- Hold: ${report.summary.hold}`, + `- Required coverage: ${report.summary.totalRequiredCoverage}`, + `- Deposits: ${report.summary.totalDeposits}`, + `- Deficit: ${report.summary.totalDeficit}`, + "", + "## Decisions", + "", + ]; + + for (const item of report.decisions) { + lines.push(`### ${item.challengeId} - ${item.decision}`); + lines.push(""); + lines.push(`- Title: ${item.title}`); + lines.push(`- Sponsor: ${item.sponsor}`); + lines.push(`- Required coverage: ${item.requiredCoverage} ${item.currency}`); + lines.push(`- Sponsor deposit: ${item.sponsorDeposit} ${item.currency}`); + lines.push(`- Deficit: ${item.deficit} ${item.currency}`); + lines.push(`- Audit digest: ${item.auditDigest}`); + lines.push(`- Evidence: ${item.evidence.join("; ")}`); + lines.push(`- Blockers: ${item.blockers.join("; ") || "none"}`); + lines.push(`- Warnings: ${item.warnings.join("; ") || "none"}`); + lines.push(`- Actions: ${item.actions.join("; ")}`); + lines.push(""); + } + + return lines.join("\n"); +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderSvgReport(report) { + const rows = report.decisions + .map((item, index) => { + const y = 190 + index * 110; + const color = + item.decision === "publish" + ? "#059669" + : item.decision === "publish_with_notice" + ? "#d97706" + : "#dc2626"; + return ` + + + + ${escapeXml(item.challengeId)} - ${escapeXml(item.decision)} + ${escapeXml(item.title.slice(0, 96))} + required ${item.requiredCoverage} / deposit ${item.sponsorDeposit} / deficit ${item.deficit} + `; + }) + .join(""); + + return ` + + + + Scientific Bounty Escrow Readiness + Publish ${report.summary.publish} / Notice ${report.summary.publish_with_notice} / Hold ${report.summary.hold} + Required ${report.summary.totalRequiredCoverage} - deposits ${report.summary.totalDeposits} - deficit ${report.summary.totalDeficit} + ${rows} +`; +} + +module.exports = { + evaluateEscrowReadiness, + evaluateEscrowReadinessBatch, + milestoneTotal, + renderMarkdownReport, + renderSvgReport, + requiredCoverage, +}; diff --git a/scientific-bounty-escrow-readiness-guard/render-video.js b/scientific-bounty-escrow-readiness-guard/render-video.js new file mode 100644 index 00000000..c52e42d7 --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/render-video.js @@ -0,0 +1,48 @@ +const fs = require("fs"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +const reportDir = path.join(__dirname, "reports"); +const jsonPath = path.join(reportDir, "escrow-readiness-review.json"); +const svgPath = path.join(reportDir, "escrow-readiness-review.svg"); +const framePath = path.join(reportDir, "escrow-readiness-review.png"); +const outputPath = path.join(reportDir, "demo.mp4"); + +if (!fs.existsSync(jsonPath) || !fs.existsSync(svgPath)) { + require("./demo"); +} + +const report = JSON.parse(fs.readFileSync(jsonPath, "utf8")); + +execFileSync( + "rsvg-convert", + ["--width", "1280", "--height", "720", "--output", framePath, svgPath], + { stdio: "inherit" } +); + +execFileSync( + "ffmpeg", + [ + "-y", + "-loop", + "1", + "-framerate", + "25", + "-i", + framePath, + "-t", + "4", + "-pix_fmt", + "yuv420p", + "-vf", + "scale=1280:720", + "-movflags", + "+faststart", + outputPath, + ], + { stdio: "inherit" } +); + +console.log( + `wrote ${path.relative(process.cwd(), outputPath)} for ${report.summary.total} checked challenges` +); diff --git a/scientific-bounty-escrow-readiness-guard/reports/demo.mp4 b/scientific-bounty-escrow-readiness-guard/reports/demo.mp4 new file mode 100644 index 00000000..52af8281 Binary files /dev/null and b/scientific-bounty-escrow-readiness-guard/reports/demo.mp4 differ diff --git a/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.json b/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.json new file mode 100644 index 00000000..df9af126 --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.json @@ -0,0 +1,87 @@ +{ + "generatedAt": "2026-05-31T06:47:18.878Z", + "summary": { + "total": 3, + "publish": 1, + "publish_with_notice": 0, + "hold": 2, + "totalRequiredCoverage": 303000, + "totalDeposits": 258000, + "totalDeficit": 48000 + }, + "decisions": [ + { + "challengeId": "challenge-funded-biomarker", + "title": "Identify plasma biomarkers from synthetic single-cell data", + "sponsor": "Northbridge Translational Lab", + "currency": "USD", + "decision": "publish", + "requiredCoverage": 138000, + "sponsorDeposit": 141000, + "surplus": 3000, + "deficit": 0, + "evidence": [ + "required coverage 138000 USD", + "sponsor deposit 141000 USD", + "funding surplus 3000 USD" + ], + "blockers": [], + "warnings": [], + "actions": [ + "publish funding-ready challenge", + "freeze escrow coverage snapshot for reviewer audit" + ], + "auditDigest": "0eac7f9b719a766a48c63b44cbb3e2e006162857f8c77bac9f2836d02998450a" + }, + { + "challengeId": "challenge-underfunded-climate", + "title": "Regional wildfire smoke forecasting benchmark", + "sponsor": "Open Climate Fund", + "currency": "USD", + "decision": "hold", + "requiredCoverage": 109000, + "sponsorDeposit": 61000, + "surplus": 0, + "deficit": 48000, + "evidence": [ + "required coverage 109000 USD", + "sponsor deposit 61000 USD" + ], + "blockers": [ + "deposit is short by 48000 USD", + "refund lock 7 days is shorter than 14 day minimum", + "milestone percentages sum to 0.85, expected 1", + "team smoke-lab requires 120% minimum member shares" + ], + "warnings": [], + "actions": [ + "hold challenge before submissions open", + "request sponsor top-up or lower published awards", + "extend refund lock before solver work starts" + ], + "auditDigest": "6ff5610fa8e52c7f7f9b9cc2ece9309b46d4872f5df3c71583256199e98b2f19" + }, + { + "challengeId": "challenge-invalid-milestones", + "title": "Quantum noise reduction synthetic benchmark", + "sponsor": "Qubit Methods Consortium", + "currency": "USD", + "decision": "hold", + "requiredCoverage": 56000, + "sponsorDeposit": 56000, + "surplus": 0, + "deficit": 0, + "evidence": [ + "required coverage 56000 USD", + "sponsor deposit 56000 USD", + "funding surplus 0 USD" + ], + "blockers": [ + "milestone percentages sum to 1.1, expected 1" + ], + "warnings": [], + "actions": [], + "auditDigest": "4b27ee358936909889641b952c810a3d1d644922deb41f5d6e38126f34a9c231" + } + ] +} diff --git a/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.md b/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.md new file mode 100644 index 00000000..752cf1f4 --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.md @@ -0,0 +1,55 @@ +# Scientific Bounty Escrow Readiness Report + +Generated: 2026-05-31T06:47:18.878Z + +## Summary + +- Challenges checked: 3 +- Publish: 1 +- Publish with notice: 0 +- Hold: 2 +- Required coverage: 303000 +- Deposits: 258000 +- Deficit: 48000 + +## Decisions + +### challenge-funded-biomarker - publish + +- Title: Identify plasma biomarkers from synthetic single-cell data +- Sponsor: Northbridge Translational Lab +- Required coverage: 138000 USD +- Sponsor deposit: 141000 USD +- Deficit: 0 USD +- Audit digest: 0eac7f9b719a766a48c63b44cbb3e2e006162857f8c77bac9f2836d02998450a +- Evidence: required coverage 138000 USD; sponsor deposit 141000 USD; funding surplus 3000 USD +- Blockers: none +- Warnings: none +- Actions: publish funding-ready challenge; freeze escrow coverage snapshot for reviewer audit + +### challenge-underfunded-climate - hold + +- Title: Regional wildfire smoke forecasting benchmark +- Sponsor: Open Climate Fund +- Required coverage: 109000 USD +- Sponsor deposit: 61000 USD +- Deficit: 48000 USD +- Audit digest: 6ff5610fa8e52c7f7f9b9cc2ece9309b46d4872f5df3c71583256199e98b2f19 +- Evidence: required coverage 109000 USD; sponsor deposit 61000 USD +- Blockers: deposit is short by 48000 USD; refund lock 7 days is shorter than 14 day minimum; milestone percentages sum to 0.85, expected 1; team smoke-lab requires 120% minimum member shares +- Warnings: none +- Actions: hold challenge before submissions open; request sponsor top-up or lower published awards; extend refund lock before solver work starts + +### challenge-invalid-milestones - hold + +- Title: Quantum noise reduction synthetic benchmark +- Sponsor: Qubit Methods Consortium +- Required coverage: 56000 USD +- Sponsor deposit: 56000 USD +- Deficit: 0 USD +- Audit digest: 4b27ee358936909889641b952c810a3d1d644922deb41f5d6e38126f34a9c231 +- Evidence: required coverage 56000 USD; sponsor deposit 56000 USD; funding surplus 0 USD +- Blockers: milestone percentages sum to 1.1, expected 1 +- Warnings: none +- Actions: + diff --git a/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.svg b/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.svg new file mode 100644 index 00000000..7215b6ce --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/reports/escrow-readiness-review.svg @@ -0,0 +1,30 @@ + + + + + Scientific Bounty Escrow Readiness + Publish 1 / Notice 0 / Hold 2 + Required 303000 - deposits 258000 - deficit 48000 + + + + + challenge-funded-biomarker - publish + Identify plasma biomarkers from synthetic single-cell data + required 138000 / deposit 141000 / deficit 0 + + + + + challenge-underfunded-climate - hold + Regional wildfire smoke forecasting benchmark + required 109000 / deposit 61000 / deficit 48000 + + + + + challenge-invalid-milestones - hold + Quantum noise reduction synthetic benchmark + required 56000 / deposit 56000 / deficit 0 + + \ No newline at end of file diff --git a/scientific-bounty-escrow-readiness-guard/sample-data.js b/scientific-bounty-escrow-readiness-guard/sample-data.js new file mode 100644 index 00000000..4f66ea12 --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/sample-data.js @@ -0,0 +1,70 @@ +const challenges = [ + { + id: "challenge-funded-biomarker", + title: "Identify plasma biomarkers from synthetic single-cell data", + sponsor: "Northbridge Translational Lab", + status: "draft", + currency: "USD", + prizeAmount: 120000, + platformReserve: 6000, + sponsorDeposit: 141000, + cancellationRefundHold: 3000, + honorableMentionBudget: 9000, + minSubmissionWindowDays: 45, + refundLockedUntilDaysAfterOpen: 21, + milestones: [ + { id: "proposal", percent: 0.15, dueDay: 15 }, + { id: "prototype", percent: 0.35, dueDay: 35 }, + { id: "final", percent: 0.5, dueDay: 60 }, + ], + teamSplits: [ + { teamId: "lab-alpha", maxMembers: 5, minMemberSharePercent: 10 }, + { teamId: "independent-team", maxMembers: 3, minMemberSharePercent: 12 }, + ], + }, + { + id: "challenge-underfunded-climate", + title: "Regional wildfire smoke forecasting benchmark", + sponsor: "Open Climate Fund", + status: "draft", + currency: "USD", + prizeAmount: 80000, + platformReserve: 4000, + sponsorDeposit: 61000, + cancellationRefundHold: 15000, + honorableMentionBudget: 10000, + minSubmissionWindowDays: 28, + refundLockedUntilDaysAfterOpen: 7, + milestones: [ + { id: "proposal", percent: 0.2, dueDay: 12 }, + { id: "prototype", percent: 0.3, dueDay: 24 }, + { id: "final", percent: 0.35, dueDay: 42 }, + ], + teamSplits: [ + { teamId: "smoke-lab", maxMembers: 6, minMemberSharePercent: 20 }, + ], + }, + { + id: "challenge-invalid-milestones", + title: "Quantum noise reduction synthetic benchmark", + sponsor: "Qubit Methods Consortium", + status: "draft", + currency: "USD", + prizeAmount: 50000, + platformReserve: 2500, + sponsorDeposit: 56000, + cancellationRefundHold: 1000, + honorableMentionBudget: 2500, + minSubmissionWindowDays: 35, + refundLockedUntilDaysAfterOpen: 20, + milestones: [ + { id: "prototype", percent: 0.4, dueDay: 20 }, + { id: "final", percent: 0.7, dueDay: 35 }, + ], + teamSplits: [ + { teamId: "quantum-team", maxMembers: 4, minMemberSharePercent: 8 }, + ], + }, +]; + +module.exports = { challenges }; diff --git a/scientific-bounty-escrow-readiness-guard/test.js b/scientific-bounty-escrow-readiness-guard/test.js new file mode 100644 index 00000000..14fe8375 --- /dev/null +++ b/scientific-bounty-escrow-readiness-guard/test.js @@ -0,0 +1,54 @@ +const assert = require("assert"); +const { + evaluateEscrowReadiness, + evaluateEscrowReadinessBatch, + milestoneTotal, + requiredCoverage, +} = require("./index"); +const { challenges } = require("./sample-data"); + +const funded = challenges.find((item) => item.id === "challenge-funded-biomarker"); +assert.strictEqual(requiredCoverage(funded), 138000); +assert.strictEqual(milestoneTotal(funded), 1); + +const fundedDecision = evaluateEscrowReadiness(funded); +assert.strictEqual(fundedDecision.decision, "publish"); +assert.strictEqual(fundedDecision.deficit, 0); +assert.ok(fundedDecision.actions.includes("publish funding-ready challenge")); + +const underfunded = evaluateEscrowReadiness( + challenges.find((item) => item.id === "challenge-underfunded-climate") +); +assert.strictEqual(underfunded.decision, "hold"); +assert.ok(underfunded.deficit > 0); +assert.ok(underfunded.blockers.some((item) => item.includes("deposit is short"))); +assert.ok(underfunded.blockers.some((item) => item.includes("refund lock"))); + +const invalidMilestones = evaluateEscrowReadiness( + challenges.find((item) => item.id === "challenge-invalid-milestones") +); +assert.strictEqual(invalidMilestones.decision, "hold"); +assert.ok( + invalidMilestones.blockers.some((item) => + item.includes("milestone percentages sum") + ) +); + +const report = evaluateEscrowReadinessBatch(challenges); +assert.deepStrictEqual( + { + total: report.summary.total, + publish: report.summary.publish, + publish_with_notice: report.summary.publish_with_notice, + hold: report.summary.hold, + }, + { + total: 3, + publish: 1, + publish_with_notice: 0, + hold: 2, + } +); +assert.strictEqual(report.decisions[0].auditDigest.length, 64); + +console.log("scientific bounty escrow readiness guard tests passed");