diff --git a/bounty-submission-quarantine-guard/demo.js b/bounty-submission-quarantine-guard/demo.js new file mode 100644 index 00000000..124052cb --- /dev/null +++ b/bounty-submission-quarantine-guard/demo.js @@ -0,0 +1,56 @@ +const fs = require("fs"); +const path = require("path"); +const { submissionBatch } = require("./sample-data"); +const { evaluateBatch } = require("./index"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const report = evaluateBatch(submissionBatch); +fs.writeFileSync(path.join(reportsDir, "submission-quarantine-report.json"), `${JSON.stringify(report, null, 2)}\n`); + +const markdown = [ + "# Scientific Bounty Submission Quarantine Report", + "", + `Batch: ${report.batchId}`, + `Generated: ${report.generatedAt}`, + "", + "## Summary", + "", + `- Submissions evaluated: ${report.summary.total}`, + `- Released to review: ${report.summary["release-to-review"] || 0}`, + `- Quarantined: ${report.summary["quarantine-submission"] || 0}`, + `- Findings: ${report.summary.findingCount}`, + "", + "## Decisions", + "", + ...report.submissions.flatMap((item) => [ + `### ${item.id}`, + "", + `- Challenge: ${item.challengeId}`, + `- Team: ${item.teamId}`, + `- Decision: ${item.decision}`, + `- Findings: ${item.findings.length ? item.findings.join("; ") : "none"}`, + `- Actions: ${item.actions.join("; ")}`, + "" + ]) +].join("\n").trim(); + +fs.writeFileSync(path.join(reportsDir, "submission-quarantine-report.md"), `${markdown}\n`); + +const svg = ` + + Submission Quarantine Guard + Scientific bounty package safety before sponsor access + ${report.submissions.map((item, index) => { + const y = 150 + index * 82; + const color = item.decision === "release-to-review" ? "#047857" : "#b45309"; + return ` + ${item.id} + ${item.decision} + ${item.teamId}, ${item.findings.length} finding(s)`; + }).join("\n ")} + +`; +fs.writeFileSync(path.join(reportsDir, "summary.svg"), svg); +console.log(JSON.stringify(report.summary, null, 2)); diff --git a/bounty-submission-quarantine-guard/index.js b/bounty-submission-quarantine-guard/index.js new file mode 100644 index 00000000..4154166a --- /dev/null +++ b/bounty-submission-quarantine-guard/index.js @@ -0,0 +1,38 @@ +function evaluateSubmission(submission) { + const findings = []; + + if (!submission.manifestComplete) findings.push("submission package manifest is incomplete"); + if (submission.artifactHashes.length === 0) findings.push("submission artifacts are missing hashes"); + if (submission.unsafeFlags.length > 0) findings.push(`unsafe artifact flags: ${submission.unsafeFlags.join(", ")}`); + if (submission.sponsorVisible && findings.length > 0) findings.push("sponsor visibility is enabled before quarantine clearance"); + if (!submission.reviewerPreview) findings.push("reviewer-safe preview is missing"); + if (submission.ipStatement === "missing") findings.push("IP handoff statement is missing"); + + const decision = findings.length === 0 ? "release-to-review" : "quarantine-submission"; + const actions = decision === "release-to-review" + ? ["release package to reviewer queue", "keep sponsor access gated until award decision"] + : ["quarantine package", "disable sponsor visibility", "request manifest and safety remediation"]; + + return { + id: submission.id, + challengeId: submission.challengeId, + teamId: submission.teamId, + decision, + findings, + actions + }; +} + +function evaluateBatch(batch) { + const submissions = batch.submissions.map(evaluateSubmission); + const summary = submissions.reduce((acc, submission) => { + acc.total += 1; + acc[submission.decision] = (acc[submission.decision] || 0) + 1; + acc.findingCount += submission.findings.length; + return acc; + }, { total: 0, findingCount: 0 }); + + return { batchId: batch.id, generatedAt: batch.generatedAt, summary, submissions }; +} + +module.exports = { evaluateSubmission, evaluateBatch }; diff --git a/bounty-submission-quarantine-guard/readme.md b/bounty-submission-quarantine-guard/readme.md new file mode 100644 index 00000000..679059aa --- /dev/null +++ b/bounty-submission-quarantine-guard/readme.md @@ -0,0 +1,33 @@ +# Scientific Bounty Submission Quarantine Guard + +This module adds a focused quarantine layer for the Scientific Bounty System. It decides whether solver submissions are safe and complete enough to reach reviewers or sponsors. + +It is intentionally separate from challenge intake, scoring and arbitration, payout routing, evidence freeze, anonymous review packets, prequalification, data-use agreements, closeout, reviewer workload, deadline fairness, and package security scanning. This guard handles submission quarantine and sponsor-visibility gating. + +## What It Checks + +- Package manifest completeness +- Artifact hash coverage +- Unsafe artifact flags such as executable binaries or macro-enabled documents +- Sponsor visibility before clearance +- Reviewer-safe preview availability +- IP handoff statement presence + +## Demo + +Run: + +```bash +node bounty-submission-quarantine-guard/test.js +node bounty-submission-quarantine-guard/demo.js +node bounty-submission-quarantine-guard/render-video.js +``` + +Generated artifacts: + +- `reports/submission-quarantine-report.json` +- `reports/submission-quarantine-report.md` +- `reports/summary.svg` +- `reports/demo.mp4` + +All data is synthetic. The module does not execute solver files, call storage providers, expose sponsor data, process payment details, use credentials, or contact SCIBASE production services. diff --git a/bounty-submission-quarantine-guard/render-video.js b/bounty-submission-quarantine-guard/render-video.js new file mode 100644 index 00000000..64b46c8f --- /dev/null +++ b/bounty-submission-quarantine-guard/render-video.js @@ -0,0 +1,38 @@ +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); +const width = 960; +const height = 540; +const ppm = path.join(reportsDir, "demo-frame.ppm"); +const mp4 = path.join(reportsDir, "demo.mp4"); + +function pixel(x, y) { + if (y < 112) return [23, 32, 51]; + if (x > 46 && x < 914 && y > 126 && y < 188) return [255, 255, 255]; + if (x > 46 && x < 914 && y > 210 && y < 272) return [255, 255, 255]; + if (x > 46 && x < 914 && y > 294 && y < 356) return [255, 255, 255]; + if (x > 46 && x < 914 && y > 378 && y < 440) return [255, 255, 255]; + if (x > 64 && x < 238 && y > 140 && y < 174) return [4, 120, 87]; + if (x > 64 && x < 238 && y > 224 && y < 258) return [180, 83, 9]; + if (x > 64 && x < 238 && y > 308 && y < 342) return [180, 83, 9]; + if (x > 64 && x < 238 && y > 392 && y < 426) return [4, 120, 87]; + return [248, 250, 252]; +} +const header = `P6\n${width} ${height}\n255\n`; +const body = Buffer.alloc(width * height * 3); +for (let y = 0; y < height; y += 1) { + for (let x = 0; x < width; x += 1) { + const offset = (y * width + x) * 3; + const [r, g, b] = pixel(x, y); + body[offset] = r; + body[offset + 1] = g; + body[offset + 2] = b; + } +} +fs.writeFileSync(ppm, Buffer.concat([Buffer.from(header), body])); +const result = spawnSync("ffmpeg", ["-y", "-loop", "1", "-framerate", "24", "-i", ppm, "-t", "5", "-vf", "format=yuv420p", "-movflags", "+faststart", mp4], { stdio: "inherit" }); +if (result.status !== 0) throw new Error("ffmpeg failed to render demo video"); +console.log(mp4); diff --git a/bounty-submission-quarantine-guard/reports/demo.mp4 b/bounty-submission-quarantine-guard/reports/demo.mp4 new file mode 100644 index 00000000..638c1cc1 Binary files /dev/null and b/bounty-submission-quarantine-guard/reports/demo.mp4 differ diff --git a/bounty-submission-quarantine-guard/reports/submission-quarantine-report.json b/bounty-submission-quarantine-guard/reports/submission-quarantine-report.json new file mode 100644 index 00000000..24904074 --- /dev/null +++ b/bounty-submission-quarantine-guard/reports/submission-quarantine-report.json @@ -0,0 +1,67 @@ +{ + "batchId": "bounty-submission-quarantine-2026-05-29", + "generatedAt": "2026-05-29T14:34:00.000Z", + "summary": { + "total": 4, + "findingCount": 7, + "release-to-review": 2, + "quarantine-submission": 2 + }, + "submissions": [ + { + "id": "sub-1", + "challengeId": "challenge-bio-17", + "teamId": "team-alpha", + "decision": "release-to-review", + "findings": [], + "actions": [ + "release package to reviewer queue", + "keep sponsor access gated until award decision" + ] + }, + { + "id": "sub-2", + "challengeId": "challenge-ml-04", + "teamId": "team-beta", + "decision": "quarantine-submission", + "findings": [ + "submission package manifest is incomplete", + "unsafe artifact flags: macro-enabled-document", + "sponsor visibility is enabled before quarantine clearance", + "reviewer-safe preview is missing", + "IP handoff statement is missing" + ], + "actions": [ + "quarantine package", + "disable sponsor visibility", + "request manifest and safety remediation" + ] + }, + { + "id": "sub-3", + "challengeId": "challenge-climate-09", + "teamId": "team-gamma", + "decision": "quarantine-submission", + "findings": [ + "submission artifacts are missing hashes", + "unsafe artifact flags: executable-binary" + ], + "actions": [ + "quarantine package", + "disable sponsor visibility", + "request manifest and safety remediation" + ] + }, + { + "id": "sub-4", + "challengeId": "challenge-quantum-02", + "teamId": "team-delta", + "decision": "release-to-review", + "findings": [], + "actions": [ + "release package to reviewer queue", + "keep sponsor access gated until award decision" + ] + } + ] +} diff --git a/bounty-submission-quarantine-guard/reports/submission-quarantine-report.md b/bounty-submission-quarantine-guard/reports/submission-quarantine-report.md new file mode 100644 index 00000000..0cf7070c --- /dev/null +++ b/bounty-submission-quarantine-guard/reports/submission-quarantine-report.md @@ -0,0 +1,45 @@ +# Scientific Bounty Submission Quarantine Report + +Batch: bounty-submission-quarantine-2026-05-29 +Generated: 2026-05-29T14:34:00.000Z + +## Summary + +- Submissions evaluated: 4 +- Released to review: 2 +- Quarantined: 2 +- Findings: 7 + +## Decisions + +### sub-1 + +- Challenge: challenge-bio-17 +- Team: team-alpha +- Decision: release-to-review +- Findings: none +- Actions: release package to reviewer queue; keep sponsor access gated until award decision + +### sub-2 + +- Challenge: challenge-ml-04 +- Team: team-beta +- Decision: quarantine-submission +- Findings: submission package manifest is incomplete; unsafe artifact flags: macro-enabled-document; sponsor visibility is enabled before quarantine clearance; reviewer-safe preview is missing; IP handoff statement is missing +- Actions: quarantine package; disable sponsor visibility; request manifest and safety remediation + +### sub-3 + +- Challenge: challenge-climate-09 +- Team: team-gamma +- Decision: quarantine-submission +- Findings: submission artifacts are missing hashes; unsafe artifact flags: executable-binary +- Actions: quarantine package; disable sponsor visibility; request manifest and safety remediation + +### sub-4 + +- Challenge: challenge-quantum-02 +- Team: team-delta +- Decision: release-to-review +- Findings: none +- Actions: release package to reviewer queue; keep sponsor access gated until award decision diff --git a/bounty-submission-quarantine-guard/reports/summary.svg b/bounty-submission-quarantine-guard/reports/summary.svg new file mode 100644 index 00000000..fe661761 --- /dev/null +++ b/bounty-submission-quarantine-guard/reports/summary.svg @@ -0,0 +1,21 @@ + + + Submission Quarantine Guard + Scientific bounty package safety before sponsor access + + sub-1 + release-to-review + team-alpha, 0 finding(s) + + sub-2 + quarantine-submission + team-beta, 5 finding(s) + + sub-3 + quarantine-submission + team-gamma, 2 finding(s) + + sub-4 + release-to-review + team-delta, 0 finding(s) + diff --git a/bounty-submission-quarantine-guard/sample-data.js b/bounty-submission-quarantine-guard/sample-data.js new file mode 100644 index 00000000..c0603258 --- /dev/null +++ b/bounty-submission-quarantine-guard/sample-data.js @@ -0,0 +1,52 @@ +const submissionBatch = { + id: "bounty-submission-quarantine-2026-05-29", + generatedAt: "2026-05-29T14:34:00.000Z", + submissions: [ + { + id: "sub-1", + challengeId: "challenge-bio-17", + teamId: "team-alpha", + manifestComplete: true, + artifactHashes: ["sha256:aa01", "sha256:bb02"], + unsafeFlags: [], + sponsorVisible: false, + reviewerPreview: true, + ipStatement: "solver-retains-until-paid" + }, + { + id: "sub-2", + challengeId: "challenge-ml-04", + teamId: "team-beta", + manifestComplete: false, + artifactHashes: ["sha256:cc03"], + unsafeFlags: ["macro-enabled-document"], + sponsorVisible: true, + reviewerPreview: false, + ipStatement: "missing" + }, + { + id: "sub-3", + challengeId: "challenge-climate-09", + teamId: "team-gamma", + manifestComplete: true, + artifactHashes: [], + unsafeFlags: ["executable-binary"], + sponsorVisible: false, + reviewerPreview: true, + ipStatement: "open-license-after-award" + }, + { + id: "sub-4", + challengeId: "challenge-quantum-02", + teamId: "team-delta", + manifestComplete: true, + artifactHashes: ["sha256:dd04"], + unsafeFlags: [], + sponsorVisible: false, + reviewerPreview: true, + ipStatement: "sponsor-transfer-on-payout" + } + ] +}; + +module.exports = { submissionBatch }; diff --git a/bounty-submission-quarantine-guard/test.js b/bounty-submission-quarantine-guard/test.js new file mode 100644 index 00000000..78c328b0 --- /dev/null +++ b/bounty-submission-quarantine-guard/test.js @@ -0,0 +1,20 @@ +const assert = require("assert"); +const { submissionBatch } = require("./sample-data"); +const { evaluateBatch } = require("./index"); + +const report = evaluateBatch(submissionBatch); + +assert.strictEqual(report.summary.total, 4); +assert.strictEqual(report.summary["release-to-review"], 2); +assert.strictEqual(report.summary["quarantine-submission"], 2); + +const beta = report.submissions.find((item) => item.id === "sub-2"); +assert(beta.findings.some((finding) => finding.includes("manifest"))); +assert(beta.findings.some((finding) => finding.includes("sponsor visibility"))); +assert(beta.findings.some((finding) => finding.includes("IP handoff"))); + +const gamma = report.submissions.find((item) => item.id === "sub-3"); +assert(gamma.findings.some((finding) => finding.includes("missing hashes"))); +assert(gamma.findings.some((finding) => finding.includes("executable-binary"))); + +console.log("bounty-submission-quarantine-guard tests passed");