From 476dc603e8f5703ca5d7c6f9c6c92c8b3978f977 Mon Sep 17 00:00:00 2001 From: davidrsdiaz Date: Thu, 28 May 2026 16:05:49 -0500 Subject: [PATCH] Add challenge deadline fairness guard --- .../README.md | 28 ++ .../demo.js | 24 + .../index.js | 443 ++++++++++++++++++ .../make-demo-video.js | 202 ++++++++ .../reports/deadline-fairness-review.json | 406 ++++++++++++++++ .../reports/deadline-fairness-review.md | 57 +++ .../reports/deadline-fairness-summary.svg | 45 ++ .../reports/demo.mp4 | Bin 0 -> 21492 bytes .../sample-data.js | 137 ++++++ .../test.js | 50 ++ 10 files changed, 1392 insertions(+) create mode 100644 scientific-bounty-deadline-fairness-guard/README.md create mode 100644 scientific-bounty-deadline-fairness-guard/demo.js create mode 100644 scientific-bounty-deadline-fairness-guard/index.js create mode 100644 scientific-bounty-deadline-fairness-guard/make-demo-video.js create mode 100644 scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-review.json create mode 100644 scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-review.md create mode 100644 scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-summary.svg create mode 100644 scientific-bounty-deadline-fairness-guard/reports/demo.mp4 create mode 100644 scientific-bounty-deadline-fairness-guard/sample-data.js create mode 100644 scientific-bounty-deadline-fairness-guard/test.js diff --git a/scientific-bounty-deadline-fairness-guard/README.md b/scientific-bounty-deadline-fairness-guard/README.md new file mode 100644 index 00000000..33cbe1e9 --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/README.md @@ -0,0 +1,28 @@ +# Scientific Bounty Deadline Fairness Guard + +Focused slice for SCIBASE issue #18, Scientific Bounty System. + +This module evaluates deadline extensions, timezone cutoffs, late submissions, and solver notice parity before a scientific bounty challenge proceeds to arbitration or award release. It is intentionally narrower than general intake, scoring, payout routing, evidence freeze, anonymous review packets, or post-closeout retention. + +## What it checks + +- Challenge deadlines are timezone-explicit and normalized before submission decisions are made. +- Sponsor deadline extensions are approved, published, and scoped to all eligible solver teams. +- Every eligible team receives the same new deadline inside the configured notice SLA. +- Submissions received after the original deadline are accepted only when a valid public extension covers them. +- Submissions received after the current deadline are rejected or held for arbitration if they were accepted. +- Evidence-freeze windows are reopened with a new snapshot instead of mutating the original frozen record. + +## Local verification + +```bash +node scientific-bounty-deadline-fairness-guard/test.js +node scientific-bounty-deadline-fairness-guard/demo.js +node scientific-bounty-deadline-fairness-guard/make-demo-video.js +``` + +Generated reviewer artifacts are written to `scientific-bounty-deadline-fairness-guard/reports/`. + +## Safety + +The fixtures are synthetic. The module does not call payment processors, use private challenge data, touch credentials, or perform any external write action. diff --git a/scientific-bounty-deadline-fairness-guard/demo.js b/scientific-bounty-deadline-fairness-guard/demo.js new file mode 100644 index 00000000..b797c0bb --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/demo.js @@ -0,0 +1,24 @@ +const fs = require("fs"); +const path = require("path"); +const { challenges } = require("./sample-data"); +const { evaluateChallenges, renderMarkdownReport, renderSvgReport } = require("./index"); + +function ensureDir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function runDemo() { + const report = evaluateChallenges(challenges); + const reportDir = path.join(__dirname, "reports"); + ensureDir(reportDir); + + fs.writeFileSync(path.join(reportDir, "deadline-fairness-review.json"), `${JSON.stringify(report, null, 2)}\n`); + fs.writeFileSync(path.join(reportDir, "deadline-fairness-review.md"), renderMarkdownReport(report)); + fs.writeFileSync(path.join(reportDir, "deadline-fairness-summary.svg"), renderSvgReport(report)); + + console.log("deadline fairness demo generated"); + console.log(`decision summary: ${JSON.stringify(report.summary)}`); + console.log(`reports: ${reportDir}`); +} + +runDemo(); diff --git a/scientific-bounty-deadline-fairness-guard/index.js b/scientific-bounty-deadline-fairness-guard/index.js new file mode 100644 index 00000000..237796e6 --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/index.js @@ -0,0 +1,443 @@ +const HOUR_MS = 60 * 60 * 1000; +const MINUTE_MS = 60 * 1000; + +function hasExplicitTimezone(value) { + return typeof value === "string" && /(?:Z|[+-]\d{2}:\d{2})$/.test(value); +} + +function parseTime(value, field, issues, context) { + if (!value || typeof value !== "string") { + issues.push({ + severity: "critical", + code: "missing-time", + field, + context, + message: `${field} is missing or not a string.`, + }); + return Number.NaN; + } + + if (!hasExplicitTimezone(value)) { + issues.push({ + severity: "high", + code: "ambiguous-timezone", + field, + context, + value, + message: `${field} must include Z or an explicit UTC offset before deadline decisions are made.`, + }); + } + + const parsed = Date.parse(value); + if (Number.isNaN(parsed)) { + issues.push({ + severity: "critical", + code: "invalid-time", + field, + context, + value, + message: `${field} is not parseable as a timestamp.`, + }); + } + + return parsed; +} + +function eligibleTeams(challenge) { + return (challenge.teams || []).filter((team) => team.eligible !== false); +} + +function latestExtensionForCurrentDeadline(challenge, issues) { + const currentDeadline = challenge.currentDeadline; + const matching = (challenge.extensionEvents || []).filter((event) => event.newDeadline === currentDeadline); + if (matching.length === 0) { + if (challenge.currentDeadline !== challenge.originalDeadline) { + issues.push({ + severity: "critical", + code: "missing-extension-event", + message: "Current deadline differs from the original deadline but no matching extension event exists.", + }); + } + return null; + } + + return matching[matching.length - 1]; +} + +function evaluateExtension(challenge, originalMs, currentMs, issues, actions) { + if (!(currentMs > originalMs)) { + return { extensionRequired: false, valid: true, event: null, noticeCoverage: [] }; + } + + const event = latestExtensionForCurrentDeadline(challenge, issues); + if (!event) { + actions.push("Hold arbitration until the deadline change has an approved extension event."); + return { extensionRequired: true, valid: false, event: null, noticeCoverage: [] }; + } + + const approvedAt = parseTime(event.approvedAt, "extension.approvedAt", issues, event.id); + const publishedAt = parseTime(event.publishedAt, "extension.publishedAt", issues, event.id); + const eventDeadlineMs = parseTime(event.newDeadline, "extension.newDeadline", issues, event.id); + let valid = true; + + if (!event.approvedBy) { + valid = false; + issues.push({ + severity: "critical", + code: "missing-extension-approval", + context: event.id, + message: "Deadline extension is missing an approving sponsor, arbiter, or challenge admin.", + }); + } + + if (event.scope !== "all-eligible-teams") { + valid = false; + issues.push({ + severity: "critical", + code: "private-extension", + context: event.id, + message: "Deadline extension is not scoped to all eligible teams.", + }); + } + + if (approvedAt > originalMs) { + valid = false; + issues.push({ + severity: "high", + code: "retroactive-extension-approval", + context: event.id, + message: "Extension was approved after the original cutoff; arbitration needs an explicit fairness review.", + }); + } + + if (publishedAt > originalMs) { + valid = false; + issues.push({ + severity: "high", + code: "extension-published-after-cutoff", + context: event.id, + message: "Extension was published after the original cutoff, so some solvers could have stopped work early.", + }); + } + + if (eventDeadlineMs !== currentMs) { + valid = false; + issues.push({ + severity: "critical", + code: "deadline-event-mismatch", + context: event.id, + message: "Extension event deadline does not match the active challenge deadline.", + }); + } + + const teams = eligibleTeams(challenge); + const noticesByTeam = new Map((event.notices || []).map((notice) => [notice.teamId, notice])); + const noticeCoverage = teams.map((team) => { + const notice = noticesByTeam.get(team.id); + if (!notice) { + valid = false; + issues.push({ + severity: "critical", + code: "missing-team-notice", + context: `${event.id}:${team.id}`, + message: `${team.name} did not receive the deadline-extension notice.`, + }); + return { teamId: team.id, status: "missing" }; + } + + const notifiedAt = parseTime(notice.notifiedAt, "notice.notifiedAt", issues, `${event.id}:${team.id}`); + const visibleDeadlineMs = parseTime( + notice.visibleDeadline, + "notice.visibleDeadline", + issues, + `${event.id}:${team.id}`, + ); + const lateByMinutes = Math.max(0, Math.ceil((notifiedAt - publishedAt) / MINUTE_MS)); + const noticeStatus = lateByMinutes > (challenge.noticeSlaMinutes || 60) ? "late" : "on-time"; + + if (noticeStatus === "late") { + valid = false; + issues.push({ + severity: "high", + code: "late-team-notice", + context: `${event.id}:${team.id}`, + message: `${team.name} received the extension notice ${lateByMinutes} minutes after publication.`, + }); + } + + if (visibleDeadlineMs !== currentMs) { + valid = false; + issues.push({ + severity: "critical", + code: "inconsistent-visible-deadline", + context: `${event.id}:${team.id}`, + message: `${team.name} saw a different deadline than the active challenge deadline.`, + }); + } + + return { + teamId: team.id, + status: noticeStatus, + lateByMinutes, + }; + }); + + if (!challenge.freezeWindow || !challenge.freezeWindow.originalSnapshotHash) { + valid = false; + issues.push({ + severity: "high", + code: "missing-original-freeze", + message: "Original evidence freeze snapshot is missing.", + }); + } + + if (!challenge.freezeWindow || !challenge.freezeWindow.reopenedAt || !challenge.freezeWindow.reopenedSnapshotHash) { + valid = false; + issues.push({ + severity: "high", + code: "missing-extension-freeze-reopen", + message: "Deadline extension did not create a new freeze snapshot for the extended window.", + }); + } + + if (!valid) { + actions.push("Hold award release until every eligible team has equal extension evidence."); + } + + return { extensionRequired: true, valid, event, noticeCoverage }; +} + +function evaluateSubmissions(challenge, originalMs, currentMs, extensionValid, issues, actions) { + return (challenge.submissions || []).map((submission) => { + const submittedAtMs = parseTime(submission.submittedAt, "submission.submittedAt", issues, submission.id); + let decision = "accept-original-window"; + let reason = "Submitted before the original cutoff."; + + if (submittedAtMs > currentMs) { + decision = submission.status === "accepted" ? "hold-accepted-after-current-cutoff" : "reject-late"; + reason = "Submitted after the active challenge cutoff."; + if (submission.status === "accepted") { + issues.push({ + severity: "critical", + code: "accepted-after-current-cutoff", + context: submission.id, + message: `${submission.id} was accepted after the current challenge deadline.`, + }); + actions.push(`Remove ${submission.id} from scoring or run an explicit arbitration exception.`); + } + } else if (submittedAtMs > originalMs) { + if (extensionValid) { + decision = "accept-valid-extension-window"; + reason = "Submitted after the original cutoff but inside a valid public extension."; + } else if (submission.status === "accepted") { + decision = "hold-accepted-under-invalid-extension"; + reason = "Submission used an extension that failed fairness checks."; + issues.push({ + severity: "critical", + code: "accepted-under-invalid-extension", + context: submission.id, + message: `${submission.id} was accepted after the original cutoff without a valid equal extension.`, + }); + } else { + decision = "reject-late"; + reason = "Submitted after the original cutoff and no valid extension applies."; + } + } + + return { + submissionId: submission.id, + teamId: submission.teamId, + submittedAt: submission.submittedAt, + status: submission.status, + decision, + reason, + }; + }); +} + +function scoreFromIssues(issues) { + const weights = { + critical: 35, + high: 18, + medium: 8, + low: 3, + }; + const deduction = issues.reduce((sum, issue) => sum + (weights[issue.severity] || 5), 0); + return Math.max(0, 100 - deduction); +} + +function decisionFromIssues(issues, submissionDecisions) { + const hasCritical = issues.some((issue) => issue.severity === "critical"); + const hasHigh = issues.some((issue) => issue.severity === "high"); + const hasRejectableLate = submissionDecisions.some((submission) => submission.decision === "reject-late"); + + if (hasCritical) return "hold-arbitration"; + if (hasHigh) return "needs-fairness-review"; + if (hasRejectableLate) return "reject-late-submissions"; + return "clear-for-scoring"; +} + +function evaluateChallenge(challenge) { + const issues = []; + const actions = []; + const originalMs = parseTime(challenge.originalDeadline, "originalDeadline", issues, challenge.id); + const currentMs = parseTime(challenge.currentDeadline, "currentDeadline", issues, challenge.id); + + if (currentMs < originalMs) { + issues.push({ + severity: "critical", + code: "deadline-shortened", + message: "Current deadline is earlier than the original deadline.", + }); + actions.push("Restore the original cutoff or collect explicit consent from all eligible teams."); + } + + const extension = evaluateExtension(challenge, originalMs, currentMs, issues, actions); + const submissions = evaluateSubmissions(challenge, originalMs, currentMs, extension.valid, issues, actions); + const score = scoreFromIssues(issues); + const decision = decisionFromIssues(issues, submissions); + + if (actions.length === 0 && decision === "clear-for-scoring") { + actions.push("Proceed to scoring with the normalized current deadline."); + } + + return { + challengeId: challenge.id, + title: challenge.title, + originalDeadline: challenge.originalDeadline, + currentDeadline: challenge.currentDeadline, + decision, + fairnessScore: score, + extension, + submissions, + issueCounts: issues.reduce((counts, issue) => { + counts[issue.severity] = (counts[issue.severity] || 0) + 1; + return counts; + }, {}), + issues, + actions: Array.from(new Set(actions)), + }; +} + +function evaluateChallenges(challenges) { + const results = challenges.map(evaluateChallenge); + const summary = { + challengeCount: results.length, + clearForScoring: results.filter((result) => result.decision === "clear-for-scoring").length, + heldForArbitration: results.filter((result) => result.decision === "hold-arbitration").length, + needsFairnessReview: results.filter((result) => result.decision === "needs-fairness-review").length, + rejectLateSubmissionSets: results.filter((result) => result.decision === "reject-late-submissions").length, + averageFairnessScore: Math.round(results.reduce((sum, result) => sum + result.fairnessScore, 0) / results.length), + }; + + return { + generatedAt: new Date("2026-05-28T00:00:00Z").toISOString(), + requirementMap: [ + "Challenge posting portal: validates timeline and extension policy before sponsor changes are published.", + "Submission engine: classifies original-window, valid-extension, and late submissions deterministically.", + "Arbitration and reward distribution: holds scoring or award release when deadline fairness evidence is incomplete.", + "Audit logs: emits reviewer-ready actions, issue codes, and freeze-window expectations.", + ], + summary, + results, + }; +} + +function escapeHtml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderMarkdownReport(report) { + const lines = [ + "# Deadline Fairness Review", + "", + `Generated: ${report.generatedAt}`, + "", + "## Summary", + "", + `- Challenges reviewed: ${report.summary.challengeCount}`, + `- Clear for scoring: ${report.summary.clearForScoring}`, + `- Held for arbitration: ${report.summary.heldForArbitration}`, + `- Needs fairness review: ${report.summary.needsFairnessReview}`, + `- Late-submission rejection sets: ${report.summary.rejectLateSubmissionSets}`, + `- Average fairness score: ${report.summary.averageFairnessScore}`, + "", + "## Requirement Map", + "", + ...report.requirementMap.map((item) => `- ${item}`), + "", + "## Challenge Decisions", + "", + ]; + + for (const result of report.results) { + lines.push(`### ${result.title}`); + lines.push(""); + lines.push(`- Decision: ${result.decision}`); + lines.push(`- Fairness score: ${result.fairnessScore}`); + lines.push(`- Original deadline: ${result.originalDeadline}`); + lines.push(`- Current deadline: ${result.currentDeadline}`); + lines.push(`- Issues: ${result.issues.length}`); + for (const action of result.actions) { + lines.push(`- Action: ${action}`); + } + lines.push(""); + } + + return `${lines.join("\n").trimEnd()}\n`; +} + +function renderSvgReport(report) { + const width = 1120; + const rowHeight = 92; + const height = 160 + report.results.length * rowHeight; + const rows = report.results + .map((result, index) => { + const y = 118 + index * rowHeight; + const barWidth = Math.max(24, Math.round(result.fairnessScore * 5.2)); + const color = + result.decision === "clear-for-scoring" + ? "#2f9e44" + : result.decision === "reject-late-submissions" + ? "#f08c00" + : "#d6336c"; + return ` + + ${escapeHtml(result.title)} + Decision: ${escapeHtml(result.decision)} | Issues: ${result.issues.length} + + + ${result.fairnessScore} + `; + }) + .join("\n"); + + return ` + + + Scientific Bounty Deadline Fairness Guard + Equal extension notice, explicit timezones, late submission handling, and freeze-window audit checks. +${rows} + +`; +} + +module.exports = { + evaluateChallenge, + evaluateChallenges, + renderMarkdownReport, + renderSvgReport, + hasExplicitTimezone, + HOUR_MS, +}; diff --git a/scientific-bounty-deadline-fairness-guard/make-demo-video.js b/scientific-bounty-deadline-fairness-guard/make-demo-video.js new file mode 100644 index 00000000..e15bc2b5 --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/make-demo-video.js @@ -0,0 +1,202 @@ +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); +const { challenges } = require("./sample-data"); +const { evaluateChallenges } = require("./index"); + +const WIDTH = 1280; +const HEIGHT = 720; +const FPS = 12; +const FRAMES = 48; + +const FONT = { + " ": ["000", "000", "000", "000", "000", "000", "000"], + "#": ["01010", "11111", "01010", "01010", "11111", "01010", "00000"], + ":": ["000", "010", "000", "000", "010", "000", "000"], + "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"], + ".": ["000", "000", "000", "000", "000", "010", "010"], + "0": ["01110", "10001", "10011", "10101", "11001", "10001", "01110"], + "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"], + "2": ["01110", "10001", "00001", "00010", "00100", "01000", "11111"], + "3": ["11110", "00001", "00001", "01110", "00001", "00001", "11110"], + "4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"], + "5": ["11111", "10000", "10000", "11110", "00001", "00001", "11110"], + "6": ["01110", "10000", "10000", "11110", "10001", "10001", "01110"], + "7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"], + "8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"], + "9": ["01110", "10001", "10001", "01111", "00001", "00001", "01110"], + A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"], + B: ["11110", "10001", "10001", "11110", "10001", "10001", "11110"], + C: ["01111", "10000", "10000", "10000", "10000", "10000", "01111"], + D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"], + E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"], + F: ["11111", "10000", "10000", "11110", "10000", "10000", "10000"], + G: ["01111", "10000", "10000", "10011", "10001", "10001", "01111"], + H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"], + I: ["11111", "00100", "00100", "00100", "00100", "00100", "11111"], + J: ["00111", "00010", "00010", "00010", "10010", "10010", "01100"], + K: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"], + L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"], + M: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"], + N: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"], + O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"], + P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"], + Q: ["01110", "10001", "10001", "10001", "10101", "10010", "01101"], + R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"], + S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"], + T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"], + U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"], + V: ["10001", "10001", "10001", "10001", "10001", "01010", "00100"], + W: ["10001", "10001", "10001", "10101", "10101", "10101", "01010"], + X: ["10001", "10001", "01010", "00100", "01010", "10001", "10001"], + Y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"], + Z: ["11111", "00001", "00010", "00100", "01000", "10000", "11111"], +}; + +function ensureDir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function color(hex) { + const clean = hex.replace("#", ""); + return [ + Number.parseInt(clean.slice(0, 2), 16), + Number.parseInt(clean.slice(2, 4), 16), + Number.parseInt(clean.slice(4, 6), 16), + ]; +} + +function fill(buffer, hex) { + const [r, g, b] = color(hex); + for (let index = 0; index < buffer.length; index += 3) { + buffer[index] = r; + buffer[index + 1] = g; + buffer[index + 2] = b; + } +} + +function rect(buffer, x, y, width, height, hex) { + const [r, g, b] = color(hex); + const startX = Math.max(0, Math.floor(x)); + const startY = Math.max(0, Math.floor(y)); + const endX = Math.min(WIDTH, Math.floor(x + width)); + const endY = Math.min(HEIGHT, Math.floor(y + height)); + for (let py = startY; py < endY; py += 1) { + for (let px = startX; px < endX; px += 1) { + const offset = (py * WIDTH + px) * 3; + buffer[offset] = r; + buffer[offset + 1] = g; + buffer[offset + 2] = b; + } + } +} + +function drawChar(buffer, ch, x, y, scale, hex) { + const glyph = FONT[ch] || FONT[" "]; + for (let row = 0; row < glyph.length; row += 1) { + for (let col = 0; col < glyph[row].length; col += 1) { + if (glyph[row][col] === "1") { + rect(buffer, x + col * scale, y + row * scale, scale, scale, hex); + } + } + } + return glyph[0].length * scale + scale; +} + +function drawText(buffer, text, x, y, scale, hex) { + let cursor = x; + for (const ch of String(text).toUpperCase()) { + cursor += drawChar(buffer, ch, cursor, y, scale, hex); + } +} + +function writePpm(file, buffer) { + const header = Buffer.from(`P6\n${WIDTH} ${HEIGHT}\n255\n`, "ascii"); + fs.writeFileSync(file, Buffer.concat([header, buffer])); +} + +function renderFrame(report, frameIndex, file) { + const buffer = Buffer.alloc(WIDTH * HEIGHT * 3); + fill(buffer, "#0b1020"); + + const progress = (frameIndex + 1) / FRAMES; + const cards = [ + { label: "CLEAR", value: report.summary.clearForScoring, color: "#1c7ed6", x: 80 }, + { label: "HOLD", value: report.summary.heldForArbitration, color: "#d6336c", x: 460 }, + { label: "REJECT", value: report.summary.rejectLateSubmissionSets, color: "#f08c00", x: 840 }, + ]; + + rect(buffer, 0, 0, WIDTH, 720, "#0b1020"); + rect(buffer, 72, 152, Math.round(1128 * progress), 5, "#4dabf7"); + rect(buffer, 70, 610, Math.round(1140 * progress), 12, "#2f9e44"); + + drawText(buffer, "SCIBASE #18", 78, 64, 8, "#ffffff"); + drawText(buffer, "DEADLINE FAIRNESS GUARD", 78, 128, 6, "#cfe8ff"); + + for (const card of cards) { + rect(buffer, card.x, 246, 340, 226, card.color); + rect(buffer, card.x + 14, 260, 312, 198, "#111827"); + drawText(buffer, card.label, card.x + 42, 296, 7, "#ffffff"); + drawText(buffer, String(card.value), card.x + 132, 364, 12, card.color); + } + + drawText(buffer, `AVG SCORE ${report.summary.averageFairnessScore}`, 118, 520, 6, "#e7f5ff"); + drawText(buffer, "SYNTHETIC DATA ONLY", 118, 566, 5, "#adb5bd"); + + writePpm(file, buffer); +} + +function ffmpegCandidates() { + return ["ffmpeg", "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg"]; +} + +function encodeFrames(framesDir, output) { + const errors = []; + for (const ffmpeg of ffmpegCandidates()) { + const result = spawnSync( + ffmpeg, + [ + "-y", + "-framerate", + String(FPS), + "-i", + path.join(framesDir, "frame-%03d.ppm"), + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + output, + ], + { encoding: "utf8" }, + ); + + if (result.status === 0 && fs.existsSync(output) && fs.statSync(output).size > 1000) { + return; + } + + const stderr = (result.stderr || result.error || "").toString(); + errors.push(`${ffmpeg}: ${stderr.split("\n").slice(-8).join("\n")}`); + } + + throw new Error(`ffmpeg failed to encode demo video:\n${errors.join("\n")}`); +} + +function main() { + const report = evaluateChallenges(challenges); + const reportDir = path.join(__dirname, "reports"); + const framesDir = path.join(reportDir, "video-frames"); + const output = path.join(reportDir, "demo.mp4"); + ensureDir(framesDir); + + for (let frame = 0; frame < FRAMES; frame += 1) { + renderFrame(report, frame, path.join(framesDir, `frame-${String(frame).padStart(3, "0")}.ppm`)); + } + + encodeFrames(framesDir, output); + fs.rmSync(framesDir, { recursive: true, force: true }); + console.log(`demo video generated: ${output}`); +} + +main(); diff --git a/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-review.json b/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-review.json new file mode 100644 index 00000000..5be92bb0 --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-review.json @@ -0,0 +1,406 @@ +{ + "generatedAt": "2026-05-28T00:00:00.000Z", + "requirementMap": [ + "Challenge posting portal: validates timeline and extension policy before sponsor changes are published.", + "Submission engine: classifies original-window, valid-extension, and late submissions deterministically.", + "Arbitration and reward distribution: holds scoring or award release when deadline fairness evidence is incomplete.", + "Audit logs: emits reviewer-ready actions, issue codes, and freeze-window expectations." + ], + "summary": { + "challengeCount": 4, + "clearForScoring": 1, + "heldForArbitration": 2, + "needsFairnessReview": 0, + "rejectLateSubmissionSets": 1, + "averageFairnessScore": 50 + }, + "results": [ + { + "challengeId": "climate-catalyst-forecast", + "title": "Regional Climate Catalyst Forecast", + "originalDeadline": "2026-06-01T17:00:00Z", + "currentDeadline": "2026-06-04T17:00:00Z", + "decision": "clear-for-scoring", + "fairnessScore": 100, + "extension": { + "extensionRequired": true, + "valid": true, + "event": { + "id": "ext-001", + "requestedBy": "sponsor:climate-nonprofit", + "approvedBy": "arbiter:public-review", + "approvedAt": "2026-05-31T18:15:00Z", + "publishedAt": "2026-05-31T18:30:00Z", + "reason": "Hosted benchmark outage affected all solvers.", + "scope": "all-eligible-teams", + "newDeadline": "2026-06-04T17:00:00Z", + "notices": [ + { + "teamId": "atlas-lab", + "notifiedAt": "2026-05-31T18:35:00Z", + "visibleDeadline": "2026-06-04T17:00:00Z" + }, + { + "teamId": "helix-models", + "notifiedAt": "2026-05-31T18:36:00Z", + "visibleDeadline": "2026-06-04T17:00:00Z" + }, + { + "teamId": "northstar-ai", + "notifiedAt": "2026-05-31T18:37:00Z", + "visibleDeadline": "2026-06-04T17:00:00Z" + } + ] + }, + "noticeCoverage": [ + { + "teamId": "atlas-lab", + "status": "on-time", + "lateByMinutes": 5 + }, + { + "teamId": "helix-models", + "status": "on-time", + "lateByMinutes": 6 + }, + { + "teamId": "northstar-ai", + "status": "on-time", + "lateByMinutes": 7 + } + ] + }, + "submissions": [ + { + "submissionId": "sub-atlas", + "teamId": "atlas-lab", + "submittedAt": "2026-06-01T16:24:00Z", + "status": "accepted", + "decision": "accept-original-window", + "reason": "Submitted before the original cutoff." + }, + { + "submissionId": "sub-helix", + "teamId": "helix-models", + "submittedAt": "2026-06-02T14:12:00Z", + "status": "accepted", + "decision": "accept-valid-extension-window", + "reason": "Submitted after the original cutoff but inside a valid public extension." + }, + { + "submissionId": "sub-northstar", + "teamId": "northstar-ai", + "submittedAt": "2026-06-04T16:45:00Z", + "status": "accepted", + "decision": "accept-valid-extension-window", + "reason": "Submitted after the original cutoff but inside a valid public extension." + } + ], + "issueCounts": {}, + "issues": [], + "actions": [ + "Proceed to scoring with the normalized current deadline." + ] + }, + { + "challengeId": "single-cell-biomarker-race", + "title": "Single-cell Biomarker Race", + "originalDeadline": "2026-06-02T18:00:00Z", + "currentDeadline": "2026-06-03T18:00:00Z", + "decision": "hold-arbitration", + "fairnessScore": 0, + "extension": { + "extensionRequired": true, + "valid": false, + "event": { + "id": "ext-private-helix", + "requestedBy": "team:helix-models", + "approvedBy": "sponsor:biotech-inc", + "approvedAt": "2026-06-02T19:30:00Z", + "publishedAt": "2026-06-02T20:15:00Z", + "reason": "Sponsor accepted one late upload after a private support thread.", + "scope": "single-team", + "teamId": "helix-models", + "newDeadline": "2026-06-03T18:00:00Z", + "notices": [ + { + "teamId": "helix-models", + "notifiedAt": "2026-06-02T20:17:00Z", + "visibleDeadline": "2026-06-03T18:00:00Z" + }, + { + "teamId": "cobalt-bio", + "notifiedAt": "2026-06-03T08:20:00Z", + "visibleDeadline": "2026-06-03T18:00:00Z" + } + ] + }, + "noticeCoverage": [ + { + "teamId": "atlas-lab", + "status": "missing" + }, + { + "teamId": "helix-models", + "status": "on-time", + "lateByMinutes": 2 + }, + { + "teamId": "cobalt-bio", + "status": "late", + "lateByMinutes": 725 + } + ] + }, + "submissions": [ + { + "submissionId": "sub-atlas", + "teamId": "atlas-lab", + "submittedAt": "2026-06-02T17:58:00Z", + "status": "accepted", + "decision": "accept-original-window", + "reason": "Submitted before the original cutoff." + }, + { + "submissionId": "sub-helix", + "teamId": "helix-models", + "submittedAt": "2026-06-03T12:03:00Z", + "status": "accepted", + "decision": "hold-accepted-under-invalid-extension", + "reason": "Submission used an extension that failed fairness checks." + }, + { + "submissionId": "sub-cobalt", + "teamId": "cobalt-bio", + "submittedAt": "2026-06-03T19:04:00Z", + "status": "accepted", + "decision": "hold-accepted-after-current-cutoff", + "reason": "Submitted after the active challenge cutoff." + } + ], + "issueCounts": { + "critical": 4, + "high": 4 + }, + "issues": [ + { + "severity": "critical", + "code": "private-extension", + "context": "ext-private-helix", + "message": "Deadline extension is not scoped to all eligible teams." + }, + { + "severity": "high", + "code": "retroactive-extension-approval", + "context": "ext-private-helix", + "message": "Extension was approved after the original cutoff; arbitration needs an explicit fairness review." + }, + { + "severity": "high", + "code": "extension-published-after-cutoff", + "context": "ext-private-helix", + "message": "Extension was published after the original cutoff, so some solvers could have stopped work early." + }, + { + "severity": "critical", + "code": "missing-team-notice", + "context": "ext-private-helix:atlas-lab", + "message": "Atlas Lab did not receive the deadline-extension notice." + }, + { + "severity": "high", + "code": "late-team-notice", + "context": "ext-private-helix:cobalt-bio", + "message": "Cobalt Bio received the extension notice 725 minutes after publication." + }, + { + "severity": "high", + "code": "missing-extension-freeze-reopen", + "message": "Deadline extension did not create a new freeze snapshot for the extended window." + }, + { + "severity": "critical", + "code": "accepted-under-invalid-extension", + "context": "sub-helix", + "message": "sub-helix was accepted after the original cutoff without a valid equal extension." + }, + { + "severity": "critical", + "code": "accepted-after-current-cutoff", + "context": "sub-cobalt", + "message": "sub-cobalt was accepted after the current challenge deadline." + } + ], + "actions": [ + "Hold award release until every eligible team has equal extension evidence.", + "Remove sub-cobalt from scoring or run an explicit arbitration exception." + ] + }, + { + "challengeId": "quantum-noise-cutoff", + "title": "Quantum Noise Reduction Sprint", + "originalDeadline": "2026-06-05T23:59:00Z", + "currentDeadline": "2026-06-05T23:59:00Z", + "decision": "reject-late-submissions", + "fairnessScore": 100, + "extension": { + "extensionRequired": false, + "valid": true, + "event": null, + "noticeCoverage": [] + }, + "submissions": [ + { + "submissionId": "sub-qubit", + "teamId": "qubit-north", + "submittedAt": "2026-06-05T23:50:00Z", + "status": "accepted", + "decision": "accept-original-window", + "reason": "Submitted before the original cutoff." + }, + { + "submissionId": "sub-phase", + "teamId": "phase-labs", + "submittedAt": "2026-06-06T00:12:00Z", + "status": "pending", + "decision": "reject-late", + "reason": "Submitted after the active challenge cutoff." + } + ], + "issueCounts": {}, + "issues": [], + "actions": [] + }, + { + "challengeId": "materials-ambiguous-cutoff", + "title": "Materials Discovery Prototype", + "originalDeadline": "2026-06-07 17:00", + "currentDeadline": "2026-06-08 17:00", + "decision": "hold-arbitration", + "fairnessScore": 0, + "extension": { + "extensionRequired": true, + "valid": false, + "event": { + "id": "ext-ambiguous-time", + "requestedBy": "sponsor:materials-co", + "approvedBy": "arbiter:challenge-admin", + "approvedAt": "2026-06-06T15:00:00Z", + "publishedAt": "2026-06-06T15:30:00Z", + "reason": "Sponsor changed uploaded dataset.", + "scope": "all-eligible-teams", + "newDeadline": "2026-06-08 17:00", + "notices": [ + { + "teamId": "lattice-lab", + "notifiedAt": "2026-06-06T15:35:00Z", + "visibleDeadline": "2026-06-08 17:00" + }, + { + "teamId": "polymer-scouts", + "notifiedAt": "2026-06-06T16:40:00Z", + "visibleDeadline": "2026-06-08 17:00" + } + ] + }, + "noticeCoverage": [ + { + "teamId": "lattice-lab", + "status": "on-time", + "lateByMinutes": 5 + }, + { + "teamId": "polymer-scouts", + "status": "late", + "lateByMinutes": 70 + } + ] + }, + "submissions": [ + { + "submissionId": "sub-lattice", + "teamId": "lattice-lab", + "submittedAt": "2026-06-08T16:30:00Z", + "status": "accepted", + "decision": "hold-accepted-under-invalid-extension", + "reason": "Submission used an extension that failed fairness checks." + }, + { + "submissionId": "sub-polymer", + "teamId": "polymer-scouts", + "submittedAt": "2026-06-08T16:55:00Z", + "status": "accepted", + "decision": "hold-accepted-under-invalid-extension", + "reason": "Submission used an extension that failed fairness checks." + } + ], + "issueCounts": { + "high": 6, + "critical": 2 + }, + "issues": [ + { + "severity": "high", + "code": "ambiguous-timezone", + "field": "originalDeadline", + "context": "materials-ambiguous-cutoff", + "value": "2026-06-07 17:00", + "message": "originalDeadline must include Z or an explicit UTC offset before deadline decisions are made." + }, + { + "severity": "high", + "code": "ambiguous-timezone", + "field": "currentDeadline", + "context": "materials-ambiguous-cutoff", + "value": "2026-06-08 17:00", + "message": "currentDeadline must include Z or an explicit UTC offset before deadline decisions are made." + }, + { + "severity": "high", + "code": "ambiguous-timezone", + "field": "extension.newDeadline", + "context": "ext-ambiguous-time", + "value": "2026-06-08 17:00", + "message": "extension.newDeadline must include Z or an explicit UTC offset before deadline decisions are made." + }, + { + "severity": "high", + "code": "ambiguous-timezone", + "field": "notice.visibleDeadline", + "context": "ext-ambiguous-time:lattice-lab", + "value": "2026-06-08 17:00", + "message": "notice.visibleDeadline must include Z or an explicit UTC offset before deadline decisions are made." + }, + { + "severity": "high", + "code": "ambiguous-timezone", + "field": "notice.visibleDeadline", + "context": "ext-ambiguous-time:polymer-scouts", + "value": "2026-06-08 17:00", + "message": "notice.visibleDeadline must include Z or an explicit UTC offset before deadline decisions are made." + }, + { + "severity": "high", + "code": "late-team-notice", + "context": "ext-ambiguous-time:polymer-scouts", + "message": "Polymer Scouts received the extension notice 70 minutes after publication." + }, + { + "severity": "critical", + "code": "accepted-under-invalid-extension", + "context": "sub-lattice", + "message": "sub-lattice was accepted after the original cutoff without a valid equal extension." + }, + { + "severity": "critical", + "code": "accepted-under-invalid-extension", + "context": "sub-polymer", + "message": "sub-polymer was accepted after the original cutoff without a valid equal extension." + } + ], + "actions": [ + "Hold award release until every eligible team has equal extension evidence." + ] + } + ] +} diff --git a/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-review.md b/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-review.md new file mode 100644 index 00000000..df1dce54 --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-review.md @@ -0,0 +1,57 @@ +# Deadline Fairness Review + +Generated: 2026-05-28T00:00:00.000Z + +## Summary + +- Challenges reviewed: 4 +- Clear for scoring: 1 +- Held for arbitration: 2 +- Needs fairness review: 0 +- Late-submission rejection sets: 1 +- Average fairness score: 50 + +## Requirement Map + +- Challenge posting portal: validates timeline and extension policy before sponsor changes are published. +- Submission engine: classifies original-window, valid-extension, and late submissions deterministically. +- Arbitration and reward distribution: holds scoring or award release when deadline fairness evidence is incomplete. +- Audit logs: emits reviewer-ready actions, issue codes, and freeze-window expectations. + +## Challenge Decisions + +### Regional Climate Catalyst Forecast + +- Decision: clear-for-scoring +- Fairness score: 100 +- Original deadline: 2026-06-01T17:00:00Z +- Current deadline: 2026-06-04T17:00:00Z +- Issues: 0 +- Action: Proceed to scoring with the normalized current deadline. + +### Single-cell Biomarker Race + +- Decision: hold-arbitration +- Fairness score: 0 +- Original deadline: 2026-06-02T18:00:00Z +- Current deadline: 2026-06-03T18:00:00Z +- Issues: 8 +- Action: Hold award release until every eligible team has equal extension evidence. +- Action: Remove sub-cobalt from scoring or run an explicit arbitration exception. + +### Quantum Noise Reduction Sprint + +- Decision: reject-late-submissions +- Fairness score: 100 +- Original deadline: 2026-06-05T23:59:00Z +- Current deadline: 2026-06-05T23:59:00Z +- Issues: 0 + +### Materials Discovery Prototype + +- Decision: hold-arbitration +- Fairness score: 0 +- Original deadline: 2026-06-07 17:00 +- Current deadline: 2026-06-08 17:00 +- Issues: 8 +- Action: Hold award release until every eligible team has equal extension evidence. diff --git a/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-summary.svg b/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-summary.svg new file mode 100644 index 00000000..318efeda --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/reports/deadline-fairness-summary.svg @@ -0,0 +1,45 @@ + + + + Scientific Bounty Deadline Fairness Guard + Equal extension notice, explicit timezones, late submission handling, and freeze-window audit checks. + + + Regional Climate Catalyst Forecast + Decision: clear-for-scoring | Issues: 0 + + + 100 + + + + Single-cell Biomarker Race + Decision: hold-arbitration | Issues: 8 + + + 0 + + + + Quantum Noise Reduction Sprint + Decision: reject-late-submissions | Issues: 0 + + + 100 + + + + Materials Discovery Prototype + Decision: hold-arbitration | Issues: 8 + + + 0 + + diff --git a/scientific-bounty-deadline-fairness-guard/reports/demo.mp4 b/scientific-bounty-deadline-fairness-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..8c525dc5599eba8d4a9d642dc2439880dac198f1 GIT binary patch literal 21492 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 exHUwKAy*Hh!#ga@1e literal 0 HcmV?d00001 diff --git a/scientific-bounty-deadline-fairness-guard/sample-data.js b/scientific-bounty-deadline-fairness-guard/sample-data.js new file mode 100644 index 00000000..ccf2b12a --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/sample-data.js @@ -0,0 +1,137 @@ +const challenges = [ + { + id: "climate-catalyst-forecast", + title: "Regional Climate Catalyst Forecast", + originalDeadline: "2026-06-01T17:00:00Z", + currentDeadline: "2026-06-04T17:00:00Z", + noticeSlaMinutes: 60, + teams: [ + { id: "atlas-lab", name: "Atlas Lab", eligible: true }, + { id: "helix-models", name: "Helix Models", eligible: true }, + { id: "northstar-ai", name: "Northstar AI", eligible: true }, + ], + extensionEvents: [ + { + id: "ext-001", + requestedBy: "sponsor:climate-nonprofit", + approvedBy: "arbiter:public-review", + approvedAt: "2026-05-31T18:15:00Z", + publishedAt: "2026-05-31T18:30:00Z", + reason: "Hosted benchmark outage affected all solvers.", + scope: "all-eligible-teams", + newDeadline: "2026-06-04T17:00:00Z", + notices: [ + { teamId: "atlas-lab", notifiedAt: "2026-05-31T18:35:00Z", visibleDeadline: "2026-06-04T17:00:00Z" }, + { teamId: "helix-models", notifiedAt: "2026-05-31T18:36:00Z", visibleDeadline: "2026-06-04T17:00:00Z" }, + { teamId: "northstar-ai", notifiedAt: "2026-05-31T18:37:00Z", visibleDeadline: "2026-06-04T17:00:00Z" }, + ], + }, + ], + freezeWindow: { + originalSnapshotHash: "sha256:91c4e3f4a6b7", + reopenedAt: "2026-05-31T18:45:00Z", + reopenedSnapshotHash: "sha256:22bd84f02ff3", + }, + submissions: [ + { id: "sub-atlas", teamId: "atlas-lab", submittedAt: "2026-06-01T16:24:00Z", status: "accepted" }, + { id: "sub-helix", teamId: "helix-models", submittedAt: "2026-06-02T14:12:00Z", status: "accepted" }, + { id: "sub-northstar", teamId: "northstar-ai", submittedAt: "2026-06-04T16:45:00Z", status: "accepted" }, + ], + }, + { + id: "single-cell-biomarker-race", + title: "Single-cell Biomarker Race", + originalDeadline: "2026-06-02T18:00:00Z", + currentDeadline: "2026-06-03T18:00:00Z", + noticeSlaMinutes: 45, + teams: [ + { id: "atlas-lab", name: "Atlas Lab", eligible: true }, + { id: "helix-models", name: "Helix Models", eligible: true }, + { id: "cobalt-bio", name: "Cobalt Bio", eligible: true }, + ], + extensionEvents: [ + { + id: "ext-private-helix", + requestedBy: "team:helix-models", + approvedBy: "sponsor:biotech-inc", + approvedAt: "2026-06-02T19:30:00Z", + publishedAt: "2026-06-02T20:15:00Z", + reason: "Sponsor accepted one late upload after a private support thread.", + scope: "single-team", + teamId: "helix-models", + newDeadline: "2026-06-03T18:00:00Z", + notices: [ + { teamId: "helix-models", notifiedAt: "2026-06-02T20:17:00Z", visibleDeadline: "2026-06-03T18:00:00Z" }, + { teamId: "cobalt-bio", notifiedAt: "2026-06-03T08:20:00Z", visibleDeadline: "2026-06-03T18:00:00Z" }, + ], + }, + ], + freezeWindow: { + originalSnapshotHash: "sha256:c0ffeeaa1111", + }, + submissions: [ + { id: "sub-atlas", teamId: "atlas-lab", submittedAt: "2026-06-02T17:58:00Z", status: "accepted" }, + { id: "sub-helix", teamId: "helix-models", submittedAt: "2026-06-03T12:03:00Z", status: "accepted" }, + { id: "sub-cobalt", teamId: "cobalt-bio", submittedAt: "2026-06-03T19:04:00Z", status: "accepted" }, + ], + }, + { + id: "quantum-noise-cutoff", + title: "Quantum Noise Reduction Sprint", + originalDeadline: "2026-06-05T23:59:00Z", + currentDeadline: "2026-06-05T23:59:00Z", + noticeSlaMinutes: 30, + teams: [ + { id: "qubit-north", name: "Qubit North", eligible: true }, + { id: "phase-labs", name: "Phase Labs", eligible: true }, + ], + extensionEvents: [], + freezeWindow: { + originalSnapshotHash: "sha256:90210aabbccd", + }, + submissions: [ + { id: "sub-qubit", teamId: "qubit-north", submittedAt: "2026-06-05T23:50:00Z", status: "accepted" }, + { id: "sub-phase", teamId: "phase-labs", submittedAt: "2026-06-06T00:12:00Z", status: "pending" }, + ], + }, + { + id: "materials-ambiguous-cutoff", + title: "Materials Discovery Prototype", + originalDeadline: "2026-06-07 17:00", + currentDeadline: "2026-06-08 17:00", + noticeSlaMinutes: 60, + teams: [ + { id: "lattice-lab", name: "Lattice Lab", eligible: true }, + { id: "polymer-scouts", name: "Polymer Scouts", eligible: true }, + ], + extensionEvents: [ + { + id: "ext-ambiguous-time", + requestedBy: "sponsor:materials-co", + approvedBy: "arbiter:challenge-admin", + approvedAt: "2026-06-06T15:00:00Z", + publishedAt: "2026-06-06T15:30:00Z", + reason: "Sponsor changed uploaded dataset.", + scope: "all-eligible-teams", + newDeadline: "2026-06-08 17:00", + notices: [ + { teamId: "lattice-lab", notifiedAt: "2026-06-06T15:35:00Z", visibleDeadline: "2026-06-08 17:00" }, + { teamId: "polymer-scouts", notifiedAt: "2026-06-06T16:40:00Z", visibleDeadline: "2026-06-08 17:00" }, + ], + }, + ], + freezeWindow: { + originalSnapshotHash: "sha256:7e57edddd123", + reopenedAt: "2026-06-06T15:40:00Z", + reopenedSnapshotHash: "sha256:8a8d9000eeee", + }, + submissions: [ + { id: "sub-lattice", teamId: "lattice-lab", submittedAt: "2026-06-08T16:30:00Z", status: "accepted" }, + { id: "sub-polymer", teamId: "polymer-scouts", submittedAt: "2026-06-08T16:55:00Z", status: "accepted" }, + ], + }, +]; + +module.exports = { + challenges, +}; diff --git a/scientific-bounty-deadline-fairness-guard/test.js b/scientific-bounty-deadline-fairness-guard/test.js new file mode 100644 index 00000000..545b0a28 --- /dev/null +++ b/scientific-bounty-deadline-fairness-guard/test.js @@ -0,0 +1,50 @@ +const assert = require("assert"); +const { challenges } = require("./sample-data"); +const { evaluateChallenge, evaluateChallenges, hasExplicitTimezone } = require("./index"); + +function byId(report, id) { + return report.results.find((result) => result.challengeId === id); +} + +function runTests() { + assert.strictEqual(hasExplicitTimezone("2026-06-01T17:00:00Z"), true); + assert.strictEqual(hasExplicitTimezone("2026-06-01T17:00:00-05:00"), true); + assert.strictEqual(hasExplicitTimezone("2026-06-01 17:00"), false); + + const report = evaluateChallenges(challenges); + assert.strictEqual(report.summary.challengeCount, 4); + + const fair = byId(report, "climate-catalyst-forecast"); + assert.strictEqual(fair.decision, "clear-for-scoring"); + assert.strictEqual(fair.fairnessScore, 100); + assert.ok(fair.submissions.some((submission) => submission.decision === "accept-valid-extension-window")); + assert.strictEqual(fair.extension.noticeCoverage.every((notice) => notice.status === "on-time"), true); + + const privateExtension = byId(report, "single-cell-biomarker-race"); + assert.strictEqual(privateExtension.decision, "hold-arbitration"); + assert.ok(privateExtension.issues.some((issue) => issue.code === "private-extension")); + assert.ok(privateExtension.issues.some((issue) => issue.code === "missing-team-notice")); + assert.ok(privateExtension.issues.some((issue) => issue.code === "accepted-after-current-cutoff")); + assert.ok( + privateExtension.submissions.some( + (submission) => submission.decision === "hold-accepted-under-invalid-extension", + ), + ); + + const late = byId(report, "quantum-noise-cutoff"); + assert.strictEqual(late.decision, "reject-late-submissions"); + assert.ok(late.submissions.some((submission) => submission.decision === "reject-late")); + + const ambiguous = byId(report, "materials-ambiguous-cutoff"); + assert.strictEqual(ambiguous.decision, "hold-arbitration"); + assert.ok(ambiguous.issues.some((issue) => issue.code === "ambiguous-timezone")); + assert.ok(ambiguous.issues.some((issue) => issue.code === "late-team-notice")); + assert.ok(ambiguous.issues.some((issue) => issue.code === "accepted-under-invalid-extension")); + + const single = evaluateChallenge(challenges[0]); + assert.strictEqual(single.actions[0], "Proceed to scoring with the normalized current deadline."); + + console.log("deadline fairness guard tests passed"); +} + +runTests();