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 00000000..8c525dc5 Binary files /dev/null and b/scientific-bounty-deadline-fairness-guard/reports/demo.mp4 differ 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();