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 [
+ ``
+ ].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, /