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 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 @@
+
\ 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, /