From 768509061d0204d0e3dbd1c12c48ad59884fd6d8 Mon Sep 17 00:00:00 2001 From: AlonePenguin <187998801+AlonePenguin@users.noreply.github.com> Date: Mon, 1 Jun 2026 06:00:27 -0400 Subject: [PATCH] Add challenge onboarding clock parity guard --- .../.gitignore | 1 + .../README.md | 42 ++ .../demo.js | 54 ++ .../index.js | 494 ++++++++++++++++++ .../make-demo-video.js | 127 +++++ .../package.json | 21 + .../reports/clean-audit.json | 26 + .../reports/demo.mp4 | Bin 0 -> 18420 bytes .../reports/manifest.json | 28 + .../reports/risky-audit.json | 179 +++++++ .../reports/risky-review.md | 39 ++ .../reports/summary.svg | 13 + .../sample-data.js | 166 ++++++ .../test.js | 46 ++ 14 files changed, 1236 insertions(+) create mode 100644 challenge-onboarding-clock-parity-guard/.gitignore create mode 100644 challenge-onboarding-clock-parity-guard/README.md create mode 100644 challenge-onboarding-clock-parity-guard/demo.js create mode 100644 challenge-onboarding-clock-parity-guard/index.js create mode 100644 challenge-onboarding-clock-parity-guard/make-demo-video.js create mode 100644 challenge-onboarding-clock-parity-guard/package.json create mode 100644 challenge-onboarding-clock-parity-guard/reports/clean-audit.json create mode 100644 challenge-onboarding-clock-parity-guard/reports/demo.mp4 create mode 100644 challenge-onboarding-clock-parity-guard/reports/manifest.json create mode 100644 challenge-onboarding-clock-parity-guard/reports/risky-audit.json create mode 100644 challenge-onboarding-clock-parity-guard/reports/risky-review.md create mode 100644 challenge-onboarding-clock-parity-guard/reports/summary.svg create mode 100644 challenge-onboarding-clock-parity-guard/sample-data.js create mode 100644 challenge-onboarding-clock-parity-guard/test.js 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 0000000000000000000000000000000000000000..ac5c059a64850c92a3a7cb4707b4f304ec50a407 GIT binary patch literal 18420 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 + + +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.");