diff --git a/challenge-onboarding-clock-parity-guard/.gitignore b/challenge-onboarding-clock-parity-guard/.gitignore new file mode 100644 index 00000000..2bf074d6 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/.gitignore @@ -0,0 +1 @@ +reports/frames/ diff --git a/challenge-onboarding-clock-parity-guard/README.md b/challenge-onboarding-clock-parity-guard/README.md new file mode 100644 index 00000000..25a12b31 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/README.md @@ -0,0 +1,42 @@ +# Challenge Onboarding Clock Parity Guard + +Self-contained reviewer artifact for SCIBASE issue #18, focused on fair workspace onboarding before a scientific bounty submission clock starts. + +This slice is intentionally narrow. It is not another challenge intake, data-room access, prequalification, escrow, scoring, payout, appeal, regulatory, or debrief module. It answers one operational fairness question: are all accepted teams equally ready before the clock begins? + +## What It Checks + +- Every accepted team has a ready workspace timestamp before the proposed clock start. +- The submission clock starts after the last team is ready, with a published buffer. +- Starter workspace template versions and starter-kit checksums match the canonical onboarding plan. +- Required tools are available to every accepted team. +- Compute and storage quotas meet the published baseline. +- Support channels are visible to every team before clock start. +- Setup blockers do not wait for first support response until after the clock begins. +- Extra privileged tools are approved or normalized across teams. + +## Local Verification + +```sh +npm run check +npm test +npm run demo +npm run verify-video +``` + +`npm run demo` writes reviewer artifacts to `reports/`: + +- `clean-audit.json` +- `risky-audit.json` +- `risky-review.md` +- `summary.svg` +- `manifest.json` +- `demo.mp4` + +## Requirement Mapping + +- Scientific bounty system: protects challenge fairness after accepted teams are chosen and before submission windows open. +- Solver workspace operations: validates starter templates, starter kits, tools, quotas, and support access. +- Equal opportunity: delays the submission clock until every accepted team has equivalent working access. +- Auditability: emits deterministic JSON, Markdown, SVG, and MP4 artifacts from synthetic local data. +- Scope hygiene: avoids credentials, external services, payment systems, private challenge data, or real participant information. diff --git a/challenge-onboarding-clock-parity-guard/demo.js b/challenge-onboarding-clock-parity-guard/demo.js new file mode 100644 index 00000000..e04d2eae --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/demo.js @@ -0,0 +1,54 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { + evaluateOnboardingClockPacket, + renderMarkdownReport, + renderSvgSummary +} = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const clean = evaluateOnboardingClockPacket(cleanPacket, { now: "2026-06-01T10:30:00.000Z" }); +const risky = evaluateOnboardingClockPacket(riskyPacket, { now: "2026-06-01T10:30:00.000Z" }); +const manifest = { + module: "challenge-onboarding-clock-parity-guard", + issue: 18, + generatedAt: "2026-06-01T10:30:00.000Z", + scenarios: [ + { + name: "clean", + status: clean.status, + fingerprint: clean.fingerprint, + findings: clean.findings.length, + nextClockStart: clean.nextClockStart + }, + { + name: "risky", + status: risky.status, + fingerprint: risky.fingerprint, + findings: risky.findings.length, + nextClockStart: risky.nextClockStart + } + ], + artifacts: [ + "reports/clean-audit.json", + "reports/risky-audit.json", + "reports/risky-review.md", + "reports/summary.svg", + "reports/demo.mp4" + ] +}; + +fs.writeFileSync(path.join(reportsDir, "clean-audit.json"), `${JSON.stringify(clean, null, 2)}\n`); +fs.writeFileSync(path.join(reportsDir, "risky-audit.json"), `${JSON.stringify(risky, null, 2)}\n`); +fs.writeFileSync(path.join(reportsDir, "risky-review.md"), renderMarkdownReport(risky, riskyPacket)); +fs.writeFileSync(path.join(reportsDir, "summary.svg"), renderSvgSummary(risky)); +fs.writeFileSync(path.join(reportsDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); + +console.log(`Clean status: ${clean.status} (${clean.fingerprint})`); +console.log(`Risky status: ${risky.status} (${risky.fingerprint})`); +console.log(`Wrote reviewer artifacts to ${reportsDir}`); diff --git a/challenge-onboarding-clock-parity-guard/index.js b/challenge-onboarding-clock-parity-guard/index.js new file mode 100644 index 00000000..3abd8031 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/index.js @@ -0,0 +1,494 @@ +"use strict"; + +const crypto = require("node:crypto"); + +const SEVERITY_ORDER = ["critical", "high", "warning", "info"]; + +function evaluateOnboardingClockPacket(packet, options = {}) { + if (!isPlainObject(packet)) { + throw new TypeError("evaluateOnboardingClockPacket expects a packet object"); + } + + const now = options.now ?? new Date().toISOString(); + const challenge = isPlainObject(packet.challenge) ? packet.challenge : {}; + const onboarding = isPlainObject(packet.onboarding) ? packet.onboarding : {}; + const clock = isPlainObject(packet.submissionClock) ? packet.submissionClock : {}; + const teams = asArray(packet.acceptedTeams); + const minimumQuota = isPlainObject(onboarding.minimumQuota) ? onboarding.minimumQuota : {}; + const requiredTools = new Set(asArray(onboarding.requiredTools).map(String)); + const requiredSupportChannels = new Set(asArray(onboarding.requiredSupportChannels).map(String)); + const findings = []; + + if (!challenge.id || !challenge.title) { + findings.push( + finding( + "PACKET_SCHEMA_MISSING_CHALLENGE", + "high", + "Onboarding packet is missing a challenge id or title.", + "Clock-start decisions need a stable challenge record.", + "challenge", + "Attach challenge metadata before evaluating onboarding parity.", + "challenge admin" + ) + ); + } + + if (!clock.startsAt) { + findings.push( + finding( + "SUBMISSION_CLOCK_MISSING_START", + "critical", + "Submission clock has no proposed start timestamp.", + "Teams cannot verify whether they received their full competition window.", + "submissionClock.startsAt", + "Set the proposed clock start after onboarding evidence is collected.", + "challenge admin" + ) + ); + } + + if (teams.length === 0) { + findings.push( + finding( + "NO_ACCEPTED_TEAMS", + "high", + "No accepted teams are present in the onboarding packet.", + "The guard cannot prove access parity without accepted-team records.", + "acceptedTeams", + "Attach accepted-team onboarding records.", + "challenge admin" + ) + ); + } + + teams.forEach((team, index) => { + inspectTeam({ + team, + index, + onboarding, + clock, + requiredTools, + requiredSupportChannels, + minimumQuota, + findings + }); + }); + + inspectParity(teams, clock, findings); + + const sortedFindings = sortFindings(findings); + const status = determineStatus(sortedFindings); + const summary = summarize(status, sortedFindings, teams.length); + const remediationActions = sortedFindings.map((item) => ({ + code: item.code, + owner: item.owner, + action: item.remediation + })); + const nextClockStart = recommendClockStart(clock, teams, sortedFindings); + const fingerprint = crypto + .createHash("sha256") + .update( + JSON.stringify({ + challenge, + onboarding, + clock, + teams: teams.map((team) => ({ + id: team.id, + acceptedAt: team.acceptedAt, + workspace: team.workspace, + support: team.support + })), + codes: sortedFindings.map((item) => item.code) + }) + ) + .digest("hex") + .slice(0, 16); + + return { + generatedAt: now, + status, + summary, + findingCounts: countBySeverity(sortedFindings), + findings: sortedFindings, + nextClockStart, + teamReadiness: teams.map((team) => ({ + teamId: team.id, + readyAt: team.workspace?.readyAt ?? null, + toolsReady: containsAll(asArray(team.workspace?.tools), requiredTools), + supportReady: containsAll(asArray(team.support?.channels), requiredSupportChannels), + starterKitChecksum: team.workspace?.starterKitChecksum ?? null + })), + remediationActions, + fingerprint + }; +} + +function renderMarkdownReport(result, packet) { + const lines = [ + "# Challenge Onboarding Clock Parity Guard", + "", + `Challenge: ${packet.challenge?.title ?? "Untitled challenge"}`, + `Status: ${result.status}`, + `Fingerprint: ${result.fingerprint}`, + `Recommended clock start: ${result.nextClockStart ?? "not ready"}`, + "", + "## Summary", + "", + result.summary, + "", + "## Team Readiness", + "" + ]; + + result.teamReadiness.forEach((team) => { + lines.push( + `- ${team.teamId}: readyAt=${team.readyAt ?? "missing"}, tools=${team.toolsReady ? "ready" : "missing"}, support=${team.supportReady ? "ready" : "missing"}` + ); + }); + + lines.push("", "## Findings", ""); + if (result.findings.length === 0) { + lines.push("- No onboarding clock blockers found."); + } else { + result.findings.forEach((item) => { + lines.push(`- ${item.severity.toUpperCase()} ${item.code}: ${item.message}`); + lines.push(` - Remediation: ${item.remediation}`); + }); + } + + return `${lines.join("\n")}\n`; +} + +function renderSvgSummary(result) { + const counts = result.findingCounts; + const critical = counts.critical ?? 0; + const high = counts.high ?? 0; + const warning = counts.warning ?? 0; + const ready = result.status === "READY"; + const statusColor = ready ? "#16794c" : result.status === "DELAY_CLOCK" ? "#a15c00" : "#a11b32"; + const holdWidth = Math.min(320, (critical + high) * 54); + const warningWidth = Math.min(220, warning * 50); + const readyWidth = ready ? 280 : Math.max(80, 280 - holdWidth); + + return [ + ``, + ``, + ``, + `Onboarding clock parity`, + `Status ${escapeXml(result.status)} - fingerprint ${escapeXml(result.fingerprint)}`, + ``, + ``, + ``, + `CLOCK`, + `Critical/high blockers: ${critical + high}`, + `Warnings: ${warning}`, + `Teams checked: ${result.teamReadiness.length}`, + `` + ].join("\n"); +} + +function inspectTeam({ team, index, onboarding, clock, requiredTools, requiredSupportChannels, minimumQuota, findings }) { + const teamId = team.id ?? `team-${index}`; + const path = `acceptedTeams[${index}]`; + const workspace = isPlainObject(team.workspace) ? team.workspace : {}; + const support = isPlainObject(team.support) ? team.support : {}; + const clockStart = parseTime(clock.startsAt); + const readyAt = parseTime(workspace.readyAt); + + if (!team.id) { + findings.push( + finding( + "TEAM_MISSING_ID", + "high", + `Accepted team at index ${index} has no stable id.`, + "Onboarding parity evidence needs stable team identifiers.", + `${path}.id`, + "Attach a stable team id before opening the submission clock.", + "challenge admin" + ) + ); + } + + if (!workspace.readyAt) { + findings.push( + finding( + "WORKSPACE_NOT_READY", + "critical", + `${teamId} has no ready workspace timestamp.`, + "The submission clock should not begin for any accepted team until every workspace is usable.", + `${path}.workspace.readyAt`, + "Provision the workspace and record the ready timestamp before starting the clock.", + "platform ops" + ) + ); + } else if (clockStart && readyAt && readyAt > clockStart) { + findings.push( + finding( + "WORKSPACE_READY_AFTER_CLOCK_START", + "critical", + `${teamId} became workspace-ready after the proposed submission clock start.`, + "Late workspace readiness silently reduces that team's competition window.", + `${path}.workspace.readyAt`, + "Delay the submission clock until this workspace and all peers are ready.", + "challenge admin" + ) + ); + } + + if (workspace.templateVersion !== onboarding.templateVersion) { + findings.push( + finding( + "WORKSPACE_TEMPLATE_MISMATCH", + "high", + `${teamId} received template ${workspace.templateVersion ?? "missing"} instead of ${onboarding.templateVersion ?? "the canonical template"}.`, + "Different starter templates can change solver workload and reproducibility.", + `${path}.workspace.templateVersion`, + "Rebuild the workspace from the canonical challenge template.", + "platform ops" + ) + ); + } + + if (workspace.starterKitChecksum !== onboarding.starterKitChecksum) { + findings.push( + finding( + "STARTER_KIT_CHECKSUM_MISMATCH", + "high", + `${teamId} received a starter kit checksum that does not match the canonical kit.`, + "Teams must start from equivalent starter notebooks, fixtures, and instructions.", + `${path}.workspace.starterKitChecksum`, + "Re-issue the canonical starter kit and refresh the checksum evidence.", + "platform ops" + ) + ); + } + + if (!containsAll(asArray(workspace.tools), requiredTools)) { + findings.push( + finding( + "REQUIRED_TOOL_ACCESS_MISSING", + "high", + `${teamId} is missing one or more required tools before clock start.`, + "Unequal tool access creates avoidable solver disadvantage.", + `${path}.workspace.tools`, + "Grant all required tools before starting or resuming the submission clock.", + "platform ops" + ) + ); + } + + const extraTools = asArray(workspace.tools).filter((tool) => !requiredTools.has(String(tool))); + if (extraTools.length > 0 && workspace.extraToolsApproved !== true) { + findings.push( + finding( + "UNAPPROVED_EXTRA_TOOL_ACCESS", + "warning", + `${teamId} has extra tool access not listed in the canonical onboarding plan.`, + "Extra tools may be fine, but they should be explicitly approved or granted to all teams.", + `${path}.workspace.tools`, + "Approve the extra tools as non-material or normalize tool access across teams.", + "challenge admin" + ) + ); + } + + if (!quotaAtLeast(workspace.computeQuota, minimumQuota)) { + findings.push( + finding( + "COMPUTE_QUOTA_BELOW_BASELINE", + "high", + `${teamId} has less compute quota than the published onboarding baseline.`, + "Solver teams need equivalent budget to run starter notebooks and benchmarks.", + `${path}.workspace.computeQuota`, + "Top up compute quota before opening the submission clock.", + "platform ops" + ) + ); + } + + if (!containsAll(asArray(support.channels), requiredSupportChannels)) { + findings.push( + finding( + "SUPPORT_CHANNEL_VISIBILITY_MISSING", + "high", + `${teamId} cannot see every required support channel.`, + "Support-channel asymmetry changes how quickly teams can resolve setup blockers.", + `${path}.support.channels`, + "Add the team to all required support channels before clock start.", + "community ops" + ) + ); + } + + if (support.firstResponseAt && clockStart && parseTime(support.firstResponseAt) > clockStart && team.setupBlockerOpen === true) { + findings.push( + finding( + "SETUP_BLOCKER_RESPONSE_AFTER_CLOCK_START", + "warning", + `${teamId} still had a setup blocker when the proposed clock started.`, + "Clock fairness is weaker if setup support starts after the challenge window begins.", + `${path}.support.firstResponseAt`, + "Pause or delay the clock until setup blockers receive an actionable response.", + "community ops" + ) + ); + } +} + +function inspectParity(teams, clock, findings) { + const clockStart = parseTime(clock.startsAt); + if (!clockStart || teams.length <= 1) { + return; + } + + const readyTimes = teams.map((team) => parseTime(team.workspace?.readyAt)).filter(Boolean).sort((left, right) => left - right); + if (readyTimes.length !== teams.length) { + findings.push( + finding( + "CLOCK_START_BEFORE_ALL_TEAMS_READY", + "critical", + "The proposed submission clock starts before every accepted team is workspace-ready.", + "The system should start or resume the challenge window only after equivalent access is verified.", + "submissionClock.startsAt", + "Move the clock start to the latest ready timestamp plus any published buffer.", + "challenge admin" + ) + ); + return; + } + + const earliest = readyTimes[0]; + const latest = readyTimes[readyTimes.length - 1]; + const skewMinutes = Math.round((latest - earliest) / 60000); + const maxSkewMinutes = Number(clock.maxReadySkewMinutes ?? 30); + if (skewMinutes > maxSkewMinutes) { + findings.push( + finding( + "WORKSPACE_READY_SKEW_EXCEEDS_POLICY", + "warning", + `Accepted-team workspaces became ready ${skewMinutes} minutes apart.`, + "Large readiness skew can be fair only if the submission clock starts after the last team is ready.", + "acceptedTeams.workspace.readyAt", + "Start the clock after the last ready timestamp or document an equal-window adjustment.", + "challenge admin" + ) + ); + } + + if (latest > clockStart) { + findings.push( + finding( + "CLOCK_START_BEFORE_ALL_TEAMS_READY", + "critical", + "The proposed submission clock starts before every accepted team is workspace-ready.", + "The system should start or resume the challenge window only after equivalent access is verified.", + "submissionClock.startsAt", + "Move the clock start to the latest ready timestamp plus any published buffer.", + "challenge admin" + ) + ); + } +} + +function recommendClockStart(clock, teams, findings) { + const hasCriticalSchema = findings.some((item) => item.code === "SUBMISSION_CLOCK_MISSING_START" || item.code === "NO_ACCEPTED_TEAMS"); + if (hasCriticalSchema) { + return null; + } + const readyTimes = teams.map((team) => parseTime(team.workspace?.readyAt)).filter(Boolean); + if (readyTimes.length !== teams.length) { + return null; + } + const bufferMinutes = Number(clock.postReadyBufferMinutes ?? 15); + const latestReady = Math.max(...readyTimes.map((time) => time.getTime())); + return new Date(latestReady + bufferMinutes * 60000).toISOString(); +} + +function determineStatus(findings) { + if (findings.some((item) => item.severity === "critical")) { + return "HOLD"; + } + if (findings.some((item) => item.severity === "high")) { + return "DELAY_CLOCK"; + } + if (findings.some((item) => item.severity === "warning")) { + return "REVISE"; + } + return "READY"; +} + +function summarize(status, findings, teamCount) { + if (status === "READY") { + return `All ${teamCount} accepted team(s) have equivalent starter workspace, tool, quota, and support access before clock start.`; + } + const counts = countBySeverity(findings); + return `Onboarding clock is ${status.toLowerCase()} with ${counts.critical ?? 0} critical, ${counts.high ?? 0} high, and ${counts.warning ?? 0} warning finding(s).`; +} + +function finding(code, severity, message, impact, path, remediation, owner = "challenge admin") { + return { code, severity, message, impact, path, remediation, owner }; +} + +function sortFindings(findings) { + return findings.sort((left, right) => { + const severityDelta = SEVERITY_ORDER.indexOf(left.severity) - SEVERITY_ORDER.indexOf(right.severity); + if (severityDelta !== 0) { + return severityDelta; + } + return left.code.localeCompare(right.code); + }); +} + +function countBySeverity(findings) { + return findings.reduce((counts, item) => { + counts[item.severity] = (counts[item.severity] ?? 0) + 1; + return counts; + }, {}); +} + +function quotaAtLeast(actual, minimum) { + if (!isPlainObject(actual)) { + return false; + } + return Object.entries(minimum).every(([key, value]) => Number(actual[key] ?? 0) >= Number(value)); +} + +function containsAll(values, required) { + const set = new Set(asArray(values).map(String)); + for (const item of required) { + if (!set.has(item)) { + return false; + } + } + return true; +} + +function parseTime(value) { + if (!value) { + return null; + } + const time = new Date(value); + return Number.isNaN(time.getTime()) ? null : time; +} + +function asArray(value) { + return Array.isArray(value) ? value : []; +} + +function isPlainObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +module.exports = { + evaluateOnboardingClockPacket, + renderMarkdownReport, + renderSvgSummary +}; diff --git a/challenge-onboarding-clock-parity-guard/make-demo-video.js b/challenge-onboarding-clock-parity-guard/make-demo-video.js new file mode 100644 index 00000000..c6f2ff1e --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/make-demo-video.js @@ -0,0 +1,127 @@ +"use strict"; + +const { execFileSync } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); + +const WIDTH = 960; +const HEIGHT = 540; +const FONT = { + A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"], + C: ["01111", "10000", "10000", "10000", "10000", "10000", "01111"], + D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"], + E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"], + G: ["01111", "10000", "10000", "10111", "10001", "10001", "01111"], + H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"], + I: ["11111", "00100", "00100", "00100", "00100", "00100", "11111"], + L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"], + O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"], + P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"], + 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", "01010", "01010", "00100"], + X: ["10001", "01010", "00100", "00100", "00100", "01010", "10001"], + Y: ["10001", "01010", "00100", "00100", "00100", "00100", "00100"] +}; + +const reportsDir = path.join(__dirname, "reports"); +const framesDir = path.join(reportsDir, "frames"); +fs.mkdirSync(framesDir, { recursive: true }); + +for (const file of fs.readdirSync(framesDir)) { + fs.unlinkSync(path.join(framesDir, file)); +} + +const slides = [ + { label: "WORKSPACES", color: [22, 121, 76], fill: 0.9 }, + { label: "DELAY CLOCK", color: [161, 92, 0], fill: 0.58 }, + { label: "FAIR START", color: [22, 121, 76], fill: 0.84 } +]; + +let frameIndex = 0; +for (const slide of slides) { + for (let i = 0; i < 8; i += 1) { + const progress = (i + 1) / 8; + const buffer = createFrame(slide, progress); + fs.writeFileSync(path.join(framesDir, `frame-${String(frameIndex).padStart(3, "0")}.ppm`), buffer); + frameIndex += 1; + } +} + +const output = path.join(reportsDir, "demo.mp4"); +execFileSync( + "ffmpeg", + [ + "-y", + "-framerate", + "8", + "-i", + path.join(framesDir, "frame-%03d.ppm"), + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + output + ], + { stdio: "ignore" } +); + +const stats = fs.statSync(output); +console.log(`Wrote ${output} (${stats.size} bytes)`); + +function createFrame(slide, progress) { + const pixels = Buffer.alloc(WIDTH * HEIGHT * 3); + fillRect(pixels, 0, 0, WIDTH, HEIGHT, [17, 24, 39]); + fillRect(pixels, 48, 48, 864, 444, [248, 250, 252]); + fillRect(pixels, 80, 190, 800, 88, [226, 232, 240]); + fillRect(pixels, 80, 190, Math.round(800 * slide.fill * progress), 88, slide.color); + fillRect(pixels, 80, 322, 240, 42, [226, 232, 240]); + fillRect(pixels, 344, 322, 240, 42, [226, 232, 240]); + fillRect(pixels, 608, 322, 240, 42, [226, 232, 240]); + fillRect(pixels, 80, 322, 80, 42, [161, 27, 50]); + fillRect(pixels, 344, 322, 150, 42, [161, 92, 0]); + fillRect(pixels, 608, 322, 230, 42, [22, 121, 76]); + drawText(pixels, "CLOCK PARITY", 82, 104, 5, [17, 24, 39]); + drawText(pixels, slide.label, 108, 214, 7, [255, 255, 255]); + drawText(pixels, "EQUAL START", 82, 414, 4, [51, 65, 85]); + return Buffer.concat([Buffer.from(`P6\n${WIDTH} ${HEIGHT}\n255\n`, "ascii"), pixels]); +} + +function fillRect(pixels, x, y, width, height, color) { + const x2 = Math.min(WIDTH, x + width); + const y2 = Math.min(HEIGHT, y + height); + for (let row = Math.max(0, y); row < y2; row += 1) { + for (let col = Math.max(0, x); col < x2; col += 1) { + const offset = (row * WIDTH + col) * 3; + pixels[offset] = color[0]; + pixels[offset + 1] = color[1]; + pixels[offset + 2] = color[2]; + } + } +} + +function drawText(pixels, text, x, y, scale, color) { + let cursor = x; + for (const rawChar of text) { + const char = rawChar.toUpperCase(); + if (char === " ") { + cursor += 4 * scale; + continue; + } + const glyph = FONT[char]; + if (!glyph) { + cursor += 6 * scale; + continue; + } + glyph.forEach((row, rowIndex) => { + for (let colIndex = 0; colIndex < row.length; colIndex += 1) { + if (row[colIndex] === "1") { + fillRect(pixels, cursor + colIndex * scale, y + rowIndex * scale, scale, scale, color); + } + } + }); + cursor += 6 * scale; + } +} diff --git a/challenge-onboarding-clock-parity-guard/package.json b/challenge-onboarding-clock-parity-guard/package.json new file mode 100644 index 00000000..26eac9b8 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/package.json @@ -0,0 +1,21 @@ +{ + "name": "challenge-onboarding-clock-parity-guard", + "version": "1.0.0", + "private": true, + "description": "Synthetic onboarding parity and clock-start guard for scientific bounty workspaces.", + "main": "index.js", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js && node --check make-demo-video.js", + "test": "node test.js", + "demo": "node demo.js && node make-demo-video.js", + "verify-video": "ffprobe -v error -show_entries stream=codec_name,width,height,duration -of default=nokey=1:noprint_wrappers=1 reports/demo.mp4" + }, + "keywords": [ + "scientific-bounty", + "onboarding", + "workspace", + "fairness", + "synthetic" + ], + "license": "MIT" +} diff --git a/challenge-onboarding-clock-parity-guard/reports/clean-audit.json b/challenge-onboarding-clock-parity-guard/reports/clean-audit.json new file mode 100644 index 00000000..49b0afd0 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/reports/clean-audit.json @@ -0,0 +1,26 @@ +{ + "generatedAt": "2026-06-01T10:30:00.000Z", + "status": "READY", + "summary": "All 2 accepted team(s) have equivalent starter workspace, tool, quota, and support access before clock start.", + "findingCounts": {}, + "findings": [], + "nextClockStart": "2026-06-03T14:58:00.000Z", + "teamReadiness": [ + { + "teamId": "team-alpha", + "readyAt": "2026-06-03T14:05:00.000Z", + "toolsReady": true, + "supportReady": true, + "starterKitChecksum": "sha256:starter-clean-abc123" + }, + { + "teamId": "team-beta", + "readyAt": "2026-06-03T14:28:00.000Z", + "toolsReady": true, + "supportReady": true, + "starterKitChecksum": "sha256:starter-clean-abc123" + } + ], + "remediationActions": [], + "fingerprint": "b4bf16e56ac7d065" +} diff --git a/challenge-onboarding-clock-parity-guard/reports/demo.mp4 b/challenge-onboarding-clock-parity-guard/reports/demo.mp4 new file mode 100644 index 00000000..ac5c059a Binary files /dev/null and b/challenge-onboarding-clock-parity-guard/reports/demo.mp4 differ diff --git a/challenge-onboarding-clock-parity-guard/reports/manifest.json b/challenge-onboarding-clock-parity-guard/reports/manifest.json new file mode 100644 index 00000000..87387aef --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/reports/manifest.json @@ -0,0 +1,28 @@ +{ + "module": "challenge-onboarding-clock-parity-guard", + "issue": 18, + "generatedAt": "2026-06-01T10:30:00.000Z", + "scenarios": [ + { + "name": "clean", + "status": "READY", + "fingerprint": "b4bf16e56ac7d065", + "findings": 0, + "nextClockStart": "2026-06-03T14:58:00.000Z" + }, + { + "name": "risky", + "status": "HOLD", + "fingerprint": "9f4574ea48b7b85b", + "findings": 10, + "nextClockStart": null + } + ], + "artifacts": [ + "reports/clean-audit.json", + "reports/risky-audit.json", + "reports/risky-review.md", + "reports/summary.svg", + "reports/demo.mp4" + ] +} diff --git a/challenge-onboarding-clock-parity-guard/reports/risky-audit.json b/challenge-onboarding-clock-parity-guard/reports/risky-audit.json new file mode 100644 index 00000000..2ad63dca --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/reports/risky-audit.json @@ -0,0 +1,179 @@ +{ + "generatedAt": "2026-06-01T10:30:00.000Z", + "status": "HOLD", + "summary": "Onboarding clock is hold with 3 critical, 5 high, and 2 warning finding(s).", + "findingCounts": { + "critical": 3, + "high": 5, + "warning": 2 + }, + "findings": [ + { + "code": "CLOCK_START_BEFORE_ALL_TEAMS_READY", + "severity": "critical", + "message": "The proposed submission clock starts before every accepted team is workspace-ready.", + "impact": "The system should start or resume the challenge window only after equivalent access is verified.", + "path": "submissionClock.startsAt", + "remediation": "Move the clock start to the latest ready timestamp plus any published buffer.", + "owner": "challenge admin" + }, + { + "code": "WORKSPACE_NOT_READY", + "severity": "critical", + "message": "team-gamma has no ready workspace timestamp.", + "impact": "The submission clock should not begin for any accepted team until every workspace is usable.", + "path": "acceptedTeams[2].workspace.readyAt", + "remediation": "Provision the workspace and record the ready timestamp before starting the clock.", + "owner": "platform ops" + }, + { + "code": "WORKSPACE_READY_AFTER_CLOCK_START", + "severity": "critical", + "message": "team-beta became workspace-ready after the proposed submission clock start.", + "impact": "Late workspace readiness silently reduces that team's competition window.", + "path": "acceptedTeams[1].workspace.readyAt", + "remediation": "Delay the submission clock until this workspace and all peers are ready.", + "owner": "challenge admin" + }, + { + "code": "COMPUTE_QUOTA_BELOW_BASELINE", + "severity": "high", + "message": "team-beta has less compute quota than the published onboarding baseline.", + "impact": "Solver teams need equivalent budget to run starter notebooks and benchmarks.", + "path": "acceptedTeams[1].workspace.computeQuota", + "remediation": "Top up compute quota before opening the submission clock.", + "owner": "platform ops" + }, + { + "code": "REQUIRED_TOOL_ACCESS_MISSING", + "severity": "high", + "message": "team-beta is missing one or more required tools before clock start.", + "impact": "Unequal tool access creates avoidable solver disadvantage.", + "path": "acceptedTeams[1].workspace.tools", + "remediation": "Grant all required tools before starting or resuming the submission clock.", + "owner": "platform ops" + }, + { + "code": "STARTER_KIT_CHECKSUM_MISMATCH", + "severity": "high", + "message": "team-beta received a starter kit checksum that does not match the canonical kit.", + "impact": "Teams must start from equivalent starter notebooks, fixtures, and instructions.", + "path": "acceptedTeams[1].workspace.starterKitChecksum", + "remediation": "Re-issue the canonical starter kit and refresh the checksum evidence.", + "owner": "platform ops" + }, + { + "code": "SUPPORT_CHANNEL_VISIBILITY_MISSING", + "severity": "high", + "message": "team-beta cannot see every required support channel.", + "impact": "Support-channel asymmetry changes how quickly teams can resolve setup blockers.", + "path": "acceptedTeams[1].support.channels", + "remediation": "Add the team to all required support channels before clock start.", + "owner": "community ops" + }, + { + "code": "WORKSPACE_TEMPLATE_MISMATCH", + "severity": "high", + "message": "team-beta received template workspace-template-2026.05 instead of workspace-template-2026.06.", + "impact": "Different starter templates can change solver workload and reproducibility.", + "path": "acceptedTeams[1].workspace.templateVersion", + "remediation": "Rebuild the workspace from the canonical challenge template.", + "owner": "platform ops" + }, + { + "code": "SETUP_BLOCKER_RESPONSE_AFTER_CLOCK_START", + "severity": "warning", + "message": "team-beta still had a setup blocker when the proposed clock started.", + "impact": "Clock fairness is weaker if setup support starts after the challenge window begins.", + "path": "acceptedTeams[1].support.firstResponseAt", + "remediation": "Pause or delay the clock until setup blockers receive an actionable response.", + "owner": "community ops" + }, + { + "code": "UNAPPROVED_EXTRA_TOOL_ACCESS", + "severity": "warning", + "message": "team-alpha has extra tool access not listed in the canonical onboarding plan.", + "impact": "Extra tools may be fine, but they should be explicitly approved or granted to all teams.", + "path": "acceptedTeams[0].workspace.tools", + "remediation": "Approve the extra tools as non-material or normalize tool access across teams.", + "owner": "challenge admin" + } + ], + "nextClockStart": null, + "teamReadiness": [ + { + "teamId": "team-alpha", + "readyAt": "2026-06-03T14:00:00.000Z", + "toolsReady": true, + "supportReady": true, + "starterKitChecksum": "sha256:starter-clean-abc123" + }, + { + "teamId": "team-beta", + "readyAt": "2026-06-03T15:35:00.000Z", + "toolsReady": false, + "supportReady": false, + "starterKitChecksum": "sha256:old-starter-kit" + }, + { + "teamId": "team-gamma", + "readyAt": null, + "toolsReady": true, + "supportReady": true, + "starterKitChecksum": "sha256:starter-clean-abc123" + } + ], + "remediationActions": [ + { + "code": "CLOCK_START_BEFORE_ALL_TEAMS_READY", + "owner": "challenge admin", + "action": "Move the clock start to the latest ready timestamp plus any published buffer." + }, + { + "code": "WORKSPACE_NOT_READY", + "owner": "platform ops", + "action": "Provision the workspace and record the ready timestamp before starting the clock." + }, + { + "code": "WORKSPACE_READY_AFTER_CLOCK_START", + "owner": "challenge admin", + "action": "Delay the submission clock until this workspace and all peers are ready." + }, + { + "code": "COMPUTE_QUOTA_BELOW_BASELINE", + "owner": "platform ops", + "action": "Top up compute quota before opening the submission clock." + }, + { + "code": "REQUIRED_TOOL_ACCESS_MISSING", + "owner": "platform ops", + "action": "Grant all required tools before starting or resuming the submission clock." + }, + { + "code": "STARTER_KIT_CHECKSUM_MISMATCH", + "owner": "platform ops", + "action": "Re-issue the canonical starter kit and refresh the checksum evidence." + }, + { + "code": "SUPPORT_CHANNEL_VISIBILITY_MISSING", + "owner": "community ops", + "action": "Add the team to all required support channels before clock start." + }, + { + "code": "WORKSPACE_TEMPLATE_MISMATCH", + "owner": "platform ops", + "action": "Rebuild the workspace from the canonical challenge template." + }, + { + "code": "SETUP_BLOCKER_RESPONSE_AFTER_CLOCK_START", + "owner": "community ops", + "action": "Pause or delay the clock until setup blockers receive an actionable response." + }, + { + "code": "UNAPPROVED_EXTRA_TOOL_ACCESS", + "owner": "challenge admin", + "action": "Approve the extra tools as non-material or normalize tool access across teams." + } + ], + "fingerprint": "9f4574ea48b7b85b" +} diff --git a/challenge-onboarding-clock-parity-guard/reports/risky-review.md b/challenge-onboarding-clock-parity-guard/reports/risky-review.md new file mode 100644 index 00000000..2685f4b3 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/reports/risky-review.md @@ -0,0 +1,39 @@ +# Challenge Onboarding Clock Parity Guard + +Challenge: Wet-lab modeling bounty onboarding +Status: HOLD +Fingerprint: 9f4574ea48b7b85b +Recommended clock start: not ready + +## Summary + +Onboarding clock is hold with 3 critical, 5 high, and 2 warning finding(s). + +## Team Readiness + +- team-alpha: readyAt=2026-06-03T14:00:00.000Z, tools=ready, support=ready +- team-beta: readyAt=2026-06-03T15:35:00.000Z, tools=missing, support=missing +- team-gamma: readyAt=missing, tools=ready, support=ready + +## Findings + +- CRITICAL CLOCK_START_BEFORE_ALL_TEAMS_READY: The proposed submission clock starts before every accepted team is workspace-ready. + - Remediation: Move the clock start to the latest ready timestamp plus any published buffer. +- CRITICAL WORKSPACE_NOT_READY: team-gamma has no ready workspace timestamp. + - Remediation: Provision the workspace and record the ready timestamp before starting the clock. +- CRITICAL WORKSPACE_READY_AFTER_CLOCK_START: team-beta became workspace-ready after the proposed submission clock start. + - Remediation: Delay the submission clock until this workspace and all peers are ready. +- HIGH COMPUTE_QUOTA_BELOW_BASELINE: team-beta has less compute quota than the published onboarding baseline. + - Remediation: Top up compute quota before opening the submission clock. +- HIGH REQUIRED_TOOL_ACCESS_MISSING: team-beta is missing one or more required tools before clock start. + - Remediation: Grant all required tools before starting or resuming the submission clock. +- HIGH STARTER_KIT_CHECKSUM_MISMATCH: team-beta received a starter kit checksum that does not match the canonical kit. + - Remediation: Re-issue the canonical starter kit and refresh the checksum evidence. +- HIGH SUPPORT_CHANNEL_VISIBILITY_MISSING: team-beta cannot see every required support channel. + - Remediation: Add the team to all required support channels before clock start. +- HIGH WORKSPACE_TEMPLATE_MISMATCH: team-beta received template workspace-template-2026.05 instead of workspace-template-2026.06. + - Remediation: Rebuild the workspace from the canonical challenge template. +- WARNING SETUP_BLOCKER_RESPONSE_AFTER_CLOCK_START: team-beta still had a setup blocker when the proposed clock started. + - Remediation: Pause or delay the clock until setup blockers receive an actionable response. +- WARNING UNAPPROVED_EXTRA_TOOL_ACCESS: team-alpha has extra tool access not listed in the canonical onboarding plan. + - Remediation: Approve the extra tools as non-material or normalize tool access across teams. diff --git a/challenge-onboarding-clock-parity-guard/reports/summary.svg b/challenge-onboarding-clock-parity-guard/reports/summary.svg new file mode 100644 index 00000000..649fd510 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/reports/summary.svg @@ -0,0 +1,13 @@ + + + +Onboarding clock parity +Status HOLD - fingerprint 9f4574ea48b7b85b + + + +CLOCK +Critical/high blockers: 8 +Warnings: 2 +Teams checked: 3 + \ No newline at end of file diff --git a/challenge-onboarding-clock-parity-guard/sample-data.js b/challenge-onboarding-clock-parity-guard/sample-data.js new file mode 100644 index 00000000..195e7559 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/sample-data.js @@ -0,0 +1,166 @@ +"use strict"; + +const cleanPacket = { + challenge: { + id: "challenge-clean-18", + title: "Protein design bounty onboarding", + sponsor: "SCIBASE synthetic sponsor" + }, + onboarding: { + templateVersion: "workspace-template-2026.06", + starterKitChecksum: "sha256:starter-clean-abc123", + requiredTools: ["notebook-runtime", "dataset-preview", "benchmark-runner"], + requiredSupportChannels: ["announcements", "setup-help", "clarifications"], + minimumQuota: { + cpuHours: 40, + gpuHours: 8, + storageGb: 25 + } + }, + submissionClock: { + startsAt: "2026-06-03T16:00:00.000Z", + postReadyBufferMinutes: 30, + maxReadySkewMinutes: 45, + durationHours: 168 + }, + acceptedTeams: [ + { + id: "team-alpha", + acceptedAt: "2026-06-03T12:00:00.000Z", + workspace: { + provisionedAt: "2026-06-03T12:12:00.000Z", + readyAt: "2026-06-03T14:05:00.000Z", + templateVersion: "workspace-template-2026.06", + starterKitChecksum: "sha256:starter-clean-abc123", + tools: ["notebook-runtime", "dataset-preview", "benchmark-runner"], + computeQuota: { + cpuHours: 42, + gpuHours: 8, + storageGb: 25 + } + }, + support: { + channels: ["announcements", "setup-help", "clarifications"], + firstResponseAt: "2026-06-03T14:12:00.000Z" + }, + setupBlockerOpen: false + }, + { + id: "team-beta", + acceptedAt: "2026-06-03T12:00:00.000Z", + workspace: { + provisionedAt: "2026-06-03T12:14:00.000Z", + readyAt: "2026-06-03T14:28:00.000Z", + templateVersion: "workspace-template-2026.06", + starterKitChecksum: "sha256:starter-clean-abc123", + tools: ["notebook-runtime", "dataset-preview", "benchmark-runner"], + computeQuota: { + cpuHours: 42, + gpuHours: 8, + storageGb: 25 + } + }, + support: { + channels: ["announcements", "setup-help", "clarifications"], + firstResponseAt: "2026-06-03T14:20:00.000Z" + }, + setupBlockerOpen: false + } + ] +}; + +const riskyPacket = { + challenge: { + id: "challenge-risky-18", + title: "Wet-lab modeling bounty onboarding", + sponsor: "SCIBASE synthetic sponsor" + }, + onboarding: { + templateVersion: "workspace-template-2026.06", + starterKitChecksum: "sha256:starter-clean-abc123", + requiredTools: ["notebook-runtime", "dataset-preview", "benchmark-runner"], + requiredSupportChannels: ["announcements", "setup-help", "clarifications"], + minimumQuota: { + cpuHours: 40, + gpuHours: 8, + storageGb: 25 + } + }, + submissionClock: { + startsAt: "2026-06-03T15:00:00.000Z", + postReadyBufferMinutes: 30, + maxReadySkewMinutes: 45, + durationHours: 168 + }, + acceptedTeams: [ + { + id: "team-alpha", + acceptedAt: "2026-06-03T12:00:00.000Z", + workspace: { + provisionedAt: "2026-06-03T12:10:00.000Z", + readyAt: "2026-06-03T14:00:00.000Z", + templateVersion: "workspace-template-2026.06", + starterKitChecksum: "sha256:starter-clean-abc123", + tools: ["notebook-runtime", "dataset-preview", "benchmark-runner", "private-gpu-pool"], + extraToolsApproved: false, + computeQuota: { + cpuHours: 40, + gpuHours: 12, + storageGb: 25 + } + }, + support: { + channels: ["announcements", "setup-help", "clarifications"], + firstResponseAt: "2026-06-03T14:02:00.000Z" + }, + setupBlockerOpen: false + }, + { + id: "team-beta", + acceptedAt: "2026-06-03T12:00:00.000Z", + workspace: { + provisionedAt: "2026-06-03T12:35:00.000Z", + readyAt: "2026-06-03T15:35:00.000Z", + templateVersion: "workspace-template-2026.05", + starterKitChecksum: "sha256:old-starter-kit", + tools: ["notebook-runtime", "dataset-preview"], + computeQuota: { + cpuHours: 28, + gpuHours: 4, + storageGb: 20 + } + }, + support: { + channels: ["announcements", "setup-help"], + firstResponseAt: "2026-06-03T15:20:00.000Z" + }, + setupBlockerOpen: true + }, + { + id: "team-gamma", + acceptedAt: "2026-06-03T12:00:00.000Z", + workspace: { + provisionedAt: "2026-06-03T13:05:00.000Z", + readyAt: null, + templateVersion: "workspace-template-2026.06", + starterKitChecksum: "sha256:starter-clean-abc123", + tools: ["notebook-runtime", "dataset-preview", "benchmark-runner"], + computeQuota: { + cpuHours: 40, + gpuHours: 8, + storageGb: 25 + } + }, + support: { + channels: ["announcements", "setup-help", "clarifications"], + firstResponseAt: null + }, + setupBlockerOpen: true + } + ] +}; + +module.exports = { + cleanPacket, + riskyPacket +}; diff --git a/challenge-onboarding-clock-parity-guard/test.js b/challenge-onboarding-clock-parity-guard/test.js new file mode 100644 index 00000000..8679cd11 --- /dev/null +++ b/challenge-onboarding-clock-parity-guard/test.js @@ -0,0 +1,46 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const { + evaluateOnboardingClockPacket, + renderMarkdownReport, + renderSvgSummary +} = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const clean = evaluateOnboardingClockPacket(cleanPacket, { now: "2026-06-01T10:30:00.000Z" }); +assert.equal(clean.status, "READY"); +assert.equal(clean.findings.length, 0); +assert.equal(clean.teamReadiness.length, 2); +assert.equal(clean.nextClockStart, "2026-06-03T14:58:00.000Z"); + +const risky = evaluateOnboardingClockPacket(riskyPacket, { now: "2026-06-01T10:30:00.000Z" }); +const riskyCodes = new Set(risky.findings.map((finding) => finding.code)); +assert.equal(risky.status, "HOLD"); +assert.ok(riskyCodes.has("WORKSPACE_NOT_READY")); +assert.ok(riskyCodes.has("WORKSPACE_READY_AFTER_CLOCK_START")); +assert.ok(riskyCodes.has("CLOCK_START_BEFORE_ALL_TEAMS_READY")); +assert.ok(riskyCodes.has("WORKSPACE_TEMPLATE_MISMATCH")); +assert.ok(riskyCodes.has("STARTER_KIT_CHECKSUM_MISMATCH")); +assert.ok(riskyCodes.has("REQUIRED_TOOL_ACCESS_MISSING")); +assert.ok(riskyCodes.has("COMPUTE_QUOTA_BELOW_BASELINE")); +assert.ok(riskyCodes.has("SUPPORT_CHANNEL_VISIBILITY_MISSING")); +assert.ok(riskyCodes.has("SETUP_BLOCKER_RESPONSE_AFTER_CLOCK_START")); +assert.ok(riskyCodes.has("UNAPPROVED_EXTRA_TOOL_ACCESS")); + +const repeatedRisky = evaluateOnboardingClockPacket(riskyPacket, { now: "2026-06-01T10:35:00.000Z" }); +assert.equal(risky.fingerprint, repeatedRisky.fingerprint); + +const markdown = renderMarkdownReport(risky, riskyPacket); +assert.match(markdown, /Challenge Onboarding Clock Parity Guard/); +assert.match(markdown, /CLOCK_START_BEFORE_ALL_TEAMS_READY/); +assert.match(markdown, /Team Readiness/); + +const svg = renderSvgSummary(risky); +assert.match(svg, / evaluateOnboardingClockPacket(null), /expects a packet object/); + +console.log("All onboarding clock parity guard tests passed.");