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 `
+`;
+}
+
+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 @@
+
+
\ 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");