diff --git a/challenge-milestone-progress-monitor/README.md b/challenge-milestone-progress-monitor/README.md
new file mode 100644
index 0000000..72ae1ab
--- /dev/null
+++ b/challenge-milestone-progress-monitor/README.md
@@ -0,0 +1,44 @@
+# Challenge Milestone Progress Monitor
+
+This is a focused Scientific Bounty System slice for SCIBASE issue #18. It audits multi-phase challenge execution after intake and before final arbitration, producing deterministic packets that show whether solver teams are ready for review, need action, or should be escalated.
+
+## Scope
+
+- Tracks required milestone evidence for each solver team.
+- Detects overdue sponsor feedback against a configured SLA.
+- Flags idle solver activity before a milestone goes stale.
+- Detects expired resubmission windows.
+- Checks reviewer packet completeness before routing to reviewers.
+- Emits stable audit digests for sponsor and arbitration records.
+
+It intentionally does not implement bounty intake, rubric scoring, anti-collusion, payout settlement, appeals, sponsor reliability, amendment consent, reviewer consensus, or IP redaction. Those slices are already represented by other submissions on the bounty thread.
+
+## Run
+
+```powershell
+node challenge-milestone-progress-monitor/test.js
+node challenge-milestone-progress-monitor/demo.js
+```
+
+The demo writes:
+
+- `challenge-milestone-progress-monitor/demo-output/audit-packets.json`
+- `challenge-milestone-progress-monitor/demo-output/demo.svg`
+
+This PR also includes the required short MP4 demo artifact:
+
+- `challenge-milestone-progress-monitor/demo-output/demo.mp4`
+
+## API
+
+```js
+const {
+ auditChallengeMilestones,
+ buildProgressSummary,
+ createAuditDigest,
+} = require("./challenge-milestone-progress-monitor");
+
+const audit = auditChallengeMilestones({ challenge, submissions, generatedAt });
+```
+
+`auditChallengeMilestones` returns a sponsor-facing audit containing packet decisions, flags, concrete solver/sponsor/reviewer actions, summary counts, and deterministic packet digests.
diff --git a/challenge-milestone-progress-monitor/acceptance-notes.md b/challenge-milestone-progress-monitor/acceptance-notes.md
new file mode 100644
index 0000000..778341d
--- /dev/null
+++ b/challenge-milestone-progress-monitor/acceptance-notes.md
@@ -0,0 +1,27 @@
+# Acceptance Notes
+
+## What This Adds
+
+- Dependency-free Node.js module under `challenge-milestone-progress-monitor/`.
+- Deterministic milestone audit packets for scientific challenge teams.
+- Tests for stalled packets, complete packets, sponsor-facing summary counts, and stable audit digests.
+- Demo JSON, SVG, and MP4 artifacts for the bounty review requirement.
+
+## Verification
+
+Use these commands from the repository root:
+
+```powershell
+node challenge-milestone-progress-monitor/test.js
+node challenge-milestone-progress-monitor/demo.js
+node --check challenge-milestone-progress-monitor/index.js
+node --check challenge-milestone-progress-monitor/test.js
+node --check challenge-milestone-progress-monitor/demo.js
+node --check challenge-milestone-progress-monitor/sample-data.js
+ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 challenge-milestone-progress-monitor/demo-output/demo.mp4
+git diff --check
+```
+
+## AI Assistance Disclosure
+
+This contribution was prepared with AI assistance from OpenAI Codex and reviewed through local deterministic tests and artifact checks before submission.
diff --git a/challenge-milestone-progress-monitor/demo-output/audit-packets.json b/challenge-milestone-progress-monitor/demo-output/audit-packets.json
new file mode 100644
index 0000000..cec9190
--- /dev/null
+++ b/challenge-milestone-progress-monitor/demo-output/audit-packets.json
@@ -0,0 +1,141 @@
+{
+ "generatedAt": "2026-05-20T12:00:00.000Z",
+ "challengeId": "SCI-BIO-42",
+ "title": "Biomarker discovery challenge",
+ "packets": [
+ {
+ "teamId": "team-aurora",
+ "teamName": "Aurora Lab",
+ "milestoneId": "prototype",
+ "milestoneLabel": "Prototype package",
+ "generatedAt": "2026-05-20T12:00:00.000Z",
+ "decision": "hold_for_escalation",
+ "flags": [
+ "MISSING_MILESTONE_EVIDENCE",
+ "SPONSOR_FEEDBACK_OVERDUE",
+ "SOLVER_IDLE_RISK",
+ "RESUBMISSION_WINDOW_EXPIRED",
+ "REVIEW_PACKET_INCOMPLETE"
+ ],
+ "riskScore": 13,
+ "deadlineState": {
+ "milestoneDueAt": "2026-05-18T12:00:00.000Z",
+ "feedbackSlaHours": 48,
+ "idleHours": 152,
+ "resubmissionHours": 72
+ },
+ "missingEvidence": [
+ "validation-report",
+ "artifact-manifest"
+ ],
+ "overdueFeedbackRequests": [
+ {
+ "milestoneId": "prototype",
+ "requestedAt": "2026-05-17T06:00:00.000Z",
+ "hoursOpen": 78,
+ "slaHours": 48
+ }
+ ],
+ "expiredResubmissionWindows": [
+ {
+ "milestoneId": "prototype",
+ "openedAt": "2026-05-16T00:00:00.000Z",
+ "hoursOpen": 108,
+ "resubmissionHours": 72
+ }
+ ],
+ "missingReviewerPacketItems": [
+ "artifact-manifest",
+ "reproduction-notes"
+ ],
+ "solverActions": [
+ "Submit validation-report, artifact-manifest evidence before milestone review",
+ "Post a solver progress update or request a scoped extension"
+ ],
+ "sponsorActions": [
+ "Resolve overdue sponsor feedback for Prototype package before arbitration",
+ "Decide whether to close or extend Prototype package resubmission window"
+ ],
+ "reviewerTasks": [
+ "Add artifact-manifest, reproduction-notes to reviewer packet before review routing"
+ ],
+ "auditDigest": "cmpm_878fb4e07adaa84b4cae08c1"
+ },
+ {
+ "teamId": "team-nova",
+ "teamName": "Nova Institute",
+ "milestoneId": "prototype",
+ "milestoneLabel": "Prototype package",
+ "generatedAt": "2026-05-20T12:00:00.000Z",
+ "decision": "ready_for_review",
+ "flags": [],
+ "riskScore": 0,
+ "deadlineState": {
+ "milestoneDueAt": "2026-05-18T12:00:00.000Z",
+ "feedbackSlaHours": 48,
+ "idleHours": 4,
+ "resubmissionHours": 72
+ },
+ "missingEvidence": [],
+ "overdueFeedbackRequests": [],
+ "expiredResubmissionWindows": [],
+ "missingReviewerPacketItems": [],
+ "solverActions": [],
+ "sponsorActions": [],
+ "reviewerTasks": [
+ "Route complete milestone packet to reviewers"
+ ],
+ "auditDigest": "cmpm_8d4e2d1f634b250de91e464d"
+ },
+ {
+ "teamId": "team-quasar",
+ "teamName": "Quasar Students",
+ "milestoneId": "final",
+ "milestoneLabel": "Final replication packet",
+ "generatedAt": "2026-05-20T12:00:00.000Z",
+ "decision": "needs_solver_action",
+ "flags": [
+ "MISSING_MILESTONE_EVIDENCE",
+ "REVIEW_PACKET_INCOMPLETE"
+ ],
+ "riskScore": 5,
+ "deadlineState": {
+ "milestoneDueAt": "2026-06-10T12:00:00.000Z",
+ "feedbackSlaHours": 48,
+ "idleHours": 18,
+ "resubmissionHours": 96
+ },
+ "missingEvidence": [
+ "independent-replication"
+ ],
+ "overdueFeedbackRequests": [],
+ "expiredResubmissionWindows": [],
+ "missingReviewerPacketItems": [
+ "replication-log",
+ "sponsor-feedback"
+ ],
+ "solverActions": [
+ "Submit independent-replication evidence before milestone review"
+ ],
+ "sponsorActions": [],
+ "reviewerTasks": [
+ "Add replication-log, sponsor-feedback to reviewer packet before review routing"
+ ],
+ "auditDigest": "cmpm_6ddb98999ede6078045762d1"
+ }
+ ],
+ "summary": {
+ "counts": {
+ "totalTeams": 3,
+ "readyForReview": 1,
+ "needsSolverAction": 1,
+ "escalations": 1,
+ "highRisk": 1
+ },
+ "nextActions": [
+ "Resolve 1 escalation packet before arbitration.",
+ "Request solver updates for 1 milestone packet.",
+ "Review 1 complete milestone packet."
+ ]
+ }
+}
diff --git a/challenge-milestone-progress-monitor/demo-output/demo.mp4 b/challenge-milestone-progress-monitor/demo-output/demo.mp4
new file mode 100644
index 0000000..4fd9157
Binary files /dev/null and b/challenge-milestone-progress-monitor/demo-output/demo.mp4 differ
diff --git a/challenge-milestone-progress-monitor/demo-output/demo.svg b/challenge-milestone-progress-monitor/demo-output/demo.svg
new file mode 100644
index 0000000..27eacd4
--- /dev/null
+++ b/challenge-milestone-progress-monitor/demo-output/demo.svg
@@ -0,0 +1,61 @@
+
\ No newline at end of file
diff --git a/challenge-milestone-progress-monitor/demo.js b/challenge-milestone-progress-monitor/demo.js
new file mode 100644
index 0000000..1d991c4
--- /dev/null
+++ b/challenge-milestone-progress-monitor/demo.js
@@ -0,0 +1,91 @@
+const fs = require("fs");
+const path = require("path");
+
+const { auditChallengeMilestones } = require("./index");
+const { challenge, submissions } = require("./sample-data");
+
+const generatedAt = "2026-05-20T12:00:00.000Z";
+const outputDir = path.join(__dirname, "demo-output");
+
+fs.mkdirSync(outputDir, { recursive: true });
+
+const audit = auditChallengeMilestones({ challenge, submissions, generatedAt });
+fs.writeFileSync(
+ path.join(outputDir, "audit-packets.json"),
+ `${JSON.stringify(audit, null, 2)}\n`
+);
+fs.writeFileSync(path.join(outputDir, "demo.svg"), buildSvg(audit));
+
+console.log(`Challenge milestone progress monitor demo`);
+console.log(`Challenge: ${audit.title}`);
+console.log(`Teams audited: ${audit.summary.counts.totalTeams}`);
+console.log(`Ready for review: ${audit.summary.counts.readyForReview}`);
+console.log(`Escalations: ${audit.summary.counts.escalations}`);
+console.log(`Needs solver action: ${audit.summary.counts.needsSolverAction}`);
+console.log(`Wrote ${path.join(outputDir, "audit-packets.json")}`);
+console.log(`Wrote ${path.join(outputDir, "demo.svg")}`);
+
+function buildSvg(audit) {
+ const rows = audit.packets
+ .map((packet, index) => {
+ const y = 198 + index * 82;
+ const color = packet.decision === "ready_for_review"
+ ? "#1f8a5b"
+ : packet.decision === "hold_for_escalation"
+ ? "#b42318"
+ : "#ad6f00";
+ const flags = packet.flags.length === 0 ? "No flags" : packet.flags.join(" | ");
+ return `
+
+
+ ${escapeXml(packet.teamName)}
+ ${escapeXml(packet.milestoneLabel)} - ${escapeXml(formatDecision(packet.decision))}
+ Risk ${packet.riskScore}
+ ${escapeXml(flags)}
+ ${escapeXml(packet.auditDigest)}
+ `;
+ })
+ .join("");
+
+ return ``;
+}
+
+function metricCard(x, y, label, value, color) {
+ return `
+
+ ${value}
+ ${escapeXml(label)}
+ `;
+}
+
+function formatDecision(decision) {
+ return decision.split("_").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
diff --git a/challenge-milestone-progress-monitor/index.js b/challenge-milestone-progress-monitor/index.js
new file mode 100644
index 0000000..cd3a2be
--- /dev/null
+++ b/challenge-milestone-progress-monitor/index.js
@@ -0,0 +1,256 @@
+const crypto = require("crypto");
+
+const FLAG_WEIGHTS = {
+ MISSING_MILESTONE_EVIDENCE: 3,
+ SPONSOR_FEEDBACK_OVERDUE: 3,
+ SOLVER_IDLE_RISK: 2,
+ RESUBMISSION_WINDOW_EXPIRED: 3,
+ REVIEW_PACKET_INCOMPLETE: 2,
+};
+
+function auditChallengeMilestones({ challenge, submissions, generatedAt = new Date().toISOString() }) {
+ if (!challenge || !Array.isArray(challenge.milestones)) {
+ throw new Error("challenge.milestones is required");
+ }
+ if (!Array.isArray(submissions)) {
+ throw new Error("submissions must be an array");
+ }
+
+ const milestoneById = new Map(challenge.milestones.map((milestone) => [milestone.id, milestone]));
+ const packets = submissions.map((submission) => {
+ const milestone = milestoneById.get(submission.currentMilestoneId);
+ if (!milestone) {
+ throw new Error(`Unknown milestone '${submission.currentMilestoneId}' for ${submission.teamId}`);
+ }
+ return buildPacket(challenge, milestone, submission, generatedAt);
+ });
+
+ const audit = {
+ generatedAt,
+ challengeId: challenge.challengeId,
+ title: challenge.title,
+ packets,
+ };
+ audit.summary = buildProgressSummary(audit);
+ return audit;
+}
+
+function buildPacket(challenge, milestone, submission, generatedAt) {
+ const evidenceTypes = new Set((submission.evidence || []).map((item) => item.type));
+ const reviewerItems = new Set(((submission.reviewerPacket || {}).items || []));
+ const flags = [];
+
+ const missingEvidence = (milestone.requiredEvidence || []).filter((type) => !evidenceTypes.has(type));
+ if (missingEvidence.length > 0) {
+ flags.push("MISSING_MILESTONE_EVIDENCE");
+ }
+
+ const overdueFeedbackRequests = getOverdueFeedbackRequests(
+ submission.sponsorFeedback || [],
+ submission.currentMilestoneId,
+ challenge.feedbackSlaHours || 48,
+ generatedAt
+ );
+ if (overdueFeedbackRequests.length > 0) {
+ flags.push("SPONSOR_FEEDBACK_OVERDUE");
+ }
+
+ const idleHours = hoursBetween(submission.lastSolverActivityAt, generatedAt);
+ if (Number.isFinite(idleHours) && idleHours > (challenge.idleRiskHours || 96)) {
+ flags.push("SOLVER_IDLE_RISK");
+ }
+
+ const expiredResubmissionWindows = getExpiredResubmissionWindows(
+ submission.resubmissions || [],
+ submission.currentMilestoneId,
+ milestone.resubmissionHours || 72,
+ generatedAt
+ );
+ if (expiredResubmissionWindows.length > 0) {
+ flags.push("RESUBMISSION_WINDOW_EXPIRED");
+ }
+
+ const missingReviewerPacketItems = (milestone.reviewerPacketRequirements || []).filter(
+ (item) => !reviewerItems.has(item)
+ );
+ if (missingReviewerPacketItems.length > 0) {
+ flags.push("REVIEW_PACKET_INCOMPLETE");
+ }
+
+ const decision = decide(flags);
+ const packet = {
+ teamId: submission.teamId,
+ teamName: submission.teamName,
+ milestoneId: milestone.id,
+ milestoneLabel: milestone.label,
+ generatedAt,
+ decision,
+ flags,
+ riskScore: flags.reduce((total, flag) => total + FLAG_WEIGHTS[flag], 0),
+ deadlineState: {
+ milestoneDueAt: milestone.dueAt,
+ feedbackSlaHours: challenge.feedbackSlaHours || 48,
+ idleHours: Number.isFinite(idleHours) ? roundHours(idleHours) : null,
+ resubmissionHours: milestone.resubmissionHours || 72,
+ },
+ missingEvidence,
+ overdueFeedbackRequests,
+ expiredResubmissionWindows,
+ missingReviewerPacketItems,
+ solverActions: buildSolverActions(missingEvidence, flags),
+ sponsorActions: buildSponsorActions(overdueFeedbackRequests, expiredResubmissionWindows, milestone),
+ reviewerTasks: buildReviewerTasks(missingReviewerPacketItems, decision),
+ };
+
+ packet.auditDigest = createAuditDigest(packet);
+ return packet;
+}
+
+function getOverdueFeedbackRequests(feedbackRequests, milestoneId, slaHours, generatedAt) {
+ return feedbackRequests
+ .filter((request) => request.milestoneId === milestoneId && !request.respondedAt)
+ .map((request) => ({
+ milestoneId: request.milestoneId,
+ requestedAt: request.requestedAt,
+ hoursOpen: roundHours(hoursBetween(request.requestedAt, generatedAt)),
+ slaHours,
+ }))
+ .filter((request) => request.hoursOpen > slaHours);
+}
+
+function getExpiredResubmissionWindows(resubmissions, milestoneId, resubmissionHours, generatedAt) {
+ return resubmissions
+ .filter((window) => window.milestoneId === milestoneId && !window.submittedAt)
+ .map((window) => ({
+ milestoneId: window.milestoneId,
+ openedAt: window.openedAt,
+ hoursOpen: roundHours(hoursBetween(window.openedAt, generatedAt)),
+ resubmissionHours,
+ }))
+ .filter((window) => window.hoursOpen > resubmissionHours);
+}
+
+function decide(flags) {
+ if (
+ flags.includes("SPONSOR_FEEDBACK_OVERDUE") ||
+ flags.includes("RESUBMISSION_WINDOW_EXPIRED")
+ ) {
+ return "hold_for_escalation";
+ }
+ if (
+ flags.includes("MISSING_MILESTONE_EVIDENCE") ||
+ flags.includes("SOLVER_IDLE_RISK") ||
+ flags.includes("REVIEW_PACKET_INCOMPLETE")
+ ) {
+ return "needs_solver_action";
+ }
+ return "ready_for_review";
+}
+
+function buildSolverActions(missingEvidence, flags) {
+ const actions = [];
+ if (missingEvidence.length > 0) {
+ actions.push(`Submit ${missingEvidence.join(", ")} evidence before milestone review`);
+ }
+ if (flags.includes("SOLVER_IDLE_RISK")) {
+ actions.push("Post a solver progress update or request a scoped extension");
+ }
+ return actions;
+}
+
+function buildSponsorActions(overdueFeedbackRequests, expiredResubmissionWindows, milestone) {
+ const actions = [];
+ if (overdueFeedbackRequests.length > 0) {
+ actions.push(
+ `Resolve overdue sponsor feedback for ${milestone.label} before arbitration`
+ );
+ }
+ if (expiredResubmissionWindows.length > 0) {
+ actions.push(`Decide whether to close or extend ${milestone.label} resubmission window`);
+ }
+ return actions;
+}
+
+function buildReviewerTasks(missingReviewerPacketItems, decision) {
+ if (decision === "ready_for_review") {
+ return ["Route complete milestone packet to reviewers"];
+ }
+ if (missingReviewerPacketItems.length === 0) {
+ return ["Keep reviewer packet locked until sponsor or solver actions clear"];
+ }
+ return [
+ `Add ${missingReviewerPacketItems.join(", ")} to reviewer packet before review routing`,
+ ];
+}
+
+function buildProgressSummary(audit) {
+ const counts = {
+ totalTeams: audit.packets.length,
+ readyForReview: audit.packets.filter((packet) => packet.decision === "ready_for_review").length,
+ needsSolverAction: audit.packets.filter((packet) => packet.decision === "needs_solver_action").length,
+ escalations: audit.packets.filter((packet) => packet.decision === "hold_for_escalation").length,
+ highRisk: audit.packets.filter((packet) => packet.riskScore >= 10).length,
+ };
+
+ const nextActions = [];
+ if (counts.escalations > 0) {
+ nextActions.push(`Resolve ${formatCount(counts.escalations, "escalation packet")} before arbitration.`);
+ }
+ if (counts.needsSolverAction > 0) {
+ nextActions.push(`Request solver updates for ${formatCount(counts.needsSolverAction, "milestone packet")}.`);
+ }
+ if (counts.readyForReview > 0) {
+ nextActions.push(`Review ${formatCount(counts.readyForReview, "complete milestone packet")}.`);
+ }
+
+ return { counts, nextActions };
+}
+
+function createAuditDigest(packet) {
+ const stableFacts = {
+ teamId: packet.teamId,
+ milestoneId: packet.milestoneId,
+ decision: packet.decision,
+ flags: [...(packet.flags || [])].sort(),
+ missingEvidence: [...(packet.missingEvidence || [])].sort(),
+ missingReviewerPacketItems: [...(packet.missingReviewerPacketItems || [])].sort(),
+ overdueFeedbackRequests: normalizeWindows(packet.overdueFeedbackRequests || []),
+ expiredResubmissionWindows: normalizeWindows(packet.expiredResubmissionWindows || []),
+ riskScore: packet.riskScore,
+ };
+ return `cmpm_${crypto.createHash("sha256").update(JSON.stringify(stableFacts)).digest("hex").slice(0, 24)}`;
+}
+
+function normalizeWindows(windows) {
+ return windows
+ .map((window) => ({
+ milestoneId: window.milestoneId,
+ openedAt: window.openedAt,
+ requestedAt: window.requestedAt,
+ hoursOpen: window.hoursOpen,
+ }))
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
+}
+
+function hoursBetween(start, end) {
+ const startDate = new Date(start);
+ const endDate = new Date(end);
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
+ return Number.NaN;
+ }
+ return (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60);
+}
+
+function roundHours(value) {
+ return Math.round(value * 10) / 10;
+}
+
+function formatCount(count, label) {
+ return `${count} ${label}${count === 1 ? "" : "s"}`;
+}
+
+module.exports = {
+ auditChallengeMilestones,
+ buildProgressSummary,
+ createAuditDigest,
+};
diff --git a/challenge-milestone-progress-monitor/requirements-map.md b/challenge-milestone-progress-monitor/requirements-map.md
new file mode 100644
index 0000000..62123bf
--- /dev/null
+++ b/challenge-milestone-progress-monitor/requirements-map.md
@@ -0,0 +1,16 @@
+# Requirements Map
+
+| Issue #18 requirement | Implementation coverage |
+| --- | --- |
+| Multi-phase challenges | Tracks `currentMilestoneId`, per-milestone evidence, due dates, and resubmission windows. |
+| Timeline and milestone deadlines | Stores milestone `dueAt`, feedback SLA hours, idle-risk hours, and resubmission-window hours in each packet. |
+| Feedback loop between submitters and sponsors | Flags sponsor feedback requests that exceed the configured SLA and generates sponsor actions. |
+| Sponsors can view submissions in a standardized evaluation dashboard | Produces normalized audit packets and a visual SVG/MP4 dashboard summary. |
+| Automated checklists for deliverables | Compares required milestone evidence and reviewer packet requirements against submitted artifacts. |
+| Optional third-party reviewers or peer validators | Routes complete packets to reviewers only when evidence and reviewer packet items are complete. |
+| Audit logs for reproducibility | Generates stable `cmpm_` audit digests from packet facts. |
+| Trust on both sides | Separates solver actions, sponsor actions, reviewer tasks, and escalation holds before arbitration. |
+
+## Non-Overlap Statement
+
+This slice is about in-flight milestone execution and progress management. It does not duplicate challenge posting/intake, private workspace privacy gates, rubric scoring, arbitration decisions, anti-collusion, appeals, escrow settlement, payout eligibility, sponsor reliability, amendment consent, reviewer consensus, or IP disclosure controls.
diff --git a/challenge-milestone-progress-monitor/sample-data.js b/challenge-milestone-progress-monitor/sample-data.js
new file mode 100644
index 0000000..b8e91a6
--- /dev/null
+++ b/challenge-milestone-progress-monitor/sample-data.js
@@ -0,0 +1,114 @@
+const challenge = {
+ challengeId: "SCI-BIO-42",
+ title: "Biomarker discovery challenge",
+ sponsor: "Northstar Therapeutics",
+ feedbackSlaHours: 48,
+ idleRiskHours: 96,
+ milestones: [
+ {
+ id: "prototype",
+ label: "Prototype package",
+ dueAt: "2026-05-18T12:00:00.000Z",
+ resubmissionHours: 72,
+ requiredEvidence: [
+ "prototype-notebook",
+ "validation-report",
+ "artifact-manifest",
+ ],
+ reviewerPacketRequirements: [
+ "artifact-manifest",
+ "rubric-alignment",
+ "reproduction-notes",
+ ],
+ },
+ {
+ id: "final",
+ label: "Final replication packet",
+ dueAt: "2026-06-10T12:00:00.000Z",
+ resubmissionHours: 96,
+ requiredEvidence: [
+ "locked-model-card",
+ "independent-replication",
+ "whitepaper",
+ ],
+ reviewerPacketRequirements: [
+ "locked-model-card",
+ "replication-log",
+ "sponsor-feedback",
+ ],
+ },
+ ],
+};
+
+const submissions = [
+ {
+ teamId: "team-aurora",
+ teamName: "Aurora Lab",
+ currentMilestoneId: "prototype",
+ lastSolverActivityAt: "2026-05-14T04:00:00.000Z",
+ evidence: [
+ { type: "prototype-notebook", artifactId: "nb-001", hash: "sha256:08b2" },
+ ],
+ sponsorFeedback: [
+ {
+ milestoneId: "prototype",
+ requestedAt: "2026-05-17T06:00:00.000Z",
+ respondedAt: null,
+ topic: "Validation cohort mismatch",
+ },
+ ],
+ resubmissions: [
+ {
+ milestoneId: "prototype",
+ openedAt: "2026-05-16T00:00:00.000Z",
+ submittedAt: null,
+ },
+ ],
+ reviewerPacket: {
+ milestoneId: "prototype",
+ items: ["rubric-alignment"],
+ },
+ },
+ {
+ teamId: "team-nova",
+ teamName: "Nova Institute",
+ currentMilestoneId: "prototype",
+ lastSolverActivityAt: "2026-05-20T08:00:00.000Z",
+ evidence: [
+ { type: "prototype-notebook", artifactId: "nb-200", hash: "sha256:1a2b" },
+ { type: "validation-report", artifactId: "vr-200", hash: "sha256:1a2c" },
+ { type: "artifact-manifest", artifactId: "am-200", hash: "sha256:1a2d" },
+ ],
+ sponsorFeedback: [
+ {
+ milestoneId: "prototype",
+ requestedAt: "2026-05-19T09:00:00.000Z",
+ respondedAt: "2026-05-19T16:00:00.000Z",
+ topic: "Assay controls",
+ },
+ ],
+ resubmissions: [],
+ reviewerPacket: {
+ milestoneId: "prototype",
+ items: ["artifact-manifest", "rubric-alignment", "reproduction-notes"],
+ },
+ },
+ {
+ teamId: "team-quasar",
+ teamName: "Quasar Students",
+ currentMilestoneId: "final",
+ lastSolverActivityAt: "2026-05-19T18:00:00.000Z",
+ evidence: [
+ { type: "locked-model-card", artifactId: "mc-441", hash: "sha256:44aa" },
+ { type: "whitepaper", artifactId: "wp-441", hash: "sha256:44ab" },
+ ],
+ sponsorFeedback: [],
+ resubmissions: [],
+ reviewerPacket: {
+ milestoneId: "final",
+ items: ["locked-model-card"],
+ },
+ },
+];
+
+module.exports = { challenge, submissions };
diff --git a/challenge-milestone-progress-monitor/test.js b/challenge-milestone-progress-monitor/test.js
new file mode 100644
index 0000000..ac11d53
--- /dev/null
+++ b/challenge-milestone-progress-monitor/test.js
@@ -0,0 +1,159 @@
+const assert = require("assert");
+
+const {
+ auditChallengeMilestones,
+ buildProgressSummary,
+ createAuditDigest,
+} = require("./index");
+
+const generatedAt = "2026-05-20T12:00:00.000Z";
+
+const challenge = {
+ challengeId: "SCI-BIO-42",
+ title: "Biomarker discovery challenge",
+ feedbackSlaHours: 48,
+ idleRiskHours: 96,
+ milestones: [
+ {
+ id: "prototype",
+ label: "Prototype package",
+ dueAt: "2026-05-18T12:00:00.000Z",
+ resubmissionHours: 72,
+ requiredEvidence: [
+ "prototype-notebook",
+ "validation-report",
+ "artifact-manifest",
+ ],
+ reviewerPacketRequirements: [
+ "artifact-manifest",
+ "rubric-alignment",
+ "reproduction-notes",
+ ],
+ },
+ ],
+};
+
+const submissions = [
+ {
+ teamId: "team-aurora",
+ teamName: "Aurora Lab",
+ currentMilestoneId: "prototype",
+ lastSolverActivityAt: "2026-05-14T04:00:00.000Z",
+ evidence: [
+ { type: "prototype-notebook", artifactId: "nb-001" },
+ ],
+ sponsorFeedback: [
+ {
+ milestoneId: "prototype",
+ requestedAt: "2026-05-17T06:00:00.000Z",
+ respondedAt: null,
+ },
+ ],
+ resubmissions: [
+ {
+ milestoneId: "prototype",
+ openedAt: "2026-05-16T00:00:00.000Z",
+ submittedAt: null,
+ },
+ ],
+ reviewerPacket: {
+ milestoneId: "prototype",
+ items: ["rubric-alignment"],
+ },
+ },
+ {
+ teamId: "team-nova",
+ teamName: "Nova Institute",
+ currentMilestoneId: "prototype",
+ lastSolverActivityAt: "2026-05-20T08:00:00.000Z",
+ evidence: [
+ { type: "prototype-notebook", artifactId: "nb-200" },
+ { type: "validation-report", artifactId: "vr-200" },
+ { type: "artifact-manifest", artifactId: "am-200" },
+ ],
+ sponsorFeedback: [
+ {
+ milestoneId: "prototype",
+ requestedAt: "2026-05-19T09:00:00.000Z",
+ respondedAt: "2026-05-19T16:00:00.000Z",
+ },
+ ],
+ resubmissions: [],
+ reviewerPacket: {
+ milestoneId: "prototype",
+ items: ["artifact-manifest", "rubric-alignment", "reproduction-notes"],
+ },
+ },
+];
+
+function test(name, fn) {
+ try {
+ fn();
+ console.log(`ok - ${name}`);
+ } catch (error) {
+ console.error(`not ok - ${name}`);
+ console.error(error);
+ process.exitCode = 1;
+ }
+}
+
+test("flags stalled milestone packets and routes concrete actions", () => {
+ const audit = auditChallengeMilestones({ challenge, submissions, generatedAt });
+ const packet = audit.packets.find((item) => item.teamId === "team-aurora");
+
+ assert(packet, "expected packet for team-aurora");
+ assert.strictEqual(packet.decision, "hold_for_escalation");
+ assert.deepStrictEqual(packet.missingEvidence, [
+ "validation-report",
+ "artifact-manifest",
+ ]);
+ assert(packet.flags.includes("MISSING_MILESTONE_EVIDENCE"));
+ assert(packet.flags.includes("SPONSOR_FEEDBACK_OVERDUE"));
+ assert(packet.flags.includes("SOLVER_IDLE_RISK"));
+ assert(packet.flags.includes("RESUBMISSION_WINDOW_EXPIRED"));
+ assert(packet.flags.includes("REVIEW_PACKET_INCOMPLETE"));
+ assert(packet.sponsorActions.some((action) => action.includes("overdue sponsor feedback")));
+ assert(packet.solverActions.some((action) => action.includes("validation-report")));
+ assert(packet.reviewerTasks.some((task) => task.includes("artifact-manifest")));
+ assert(packet.riskScore >= 10);
+});
+
+test("marks complete milestone packets ready for review", () => {
+ const audit = auditChallengeMilestones({ challenge, submissions, generatedAt });
+ const packet = audit.packets.find((item) => item.teamId === "team-nova");
+
+ assert(packet, "expected packet for team-nova");
+ assert.strictEqual(packet.decision, "ready_for_review");
+ assert.deepStrictEqual(packet.flags, []);
+ assert.deepStrictEqual(packet.missingEvidence, []);
+ assert.strictEqual(packet.riskScore, 0);
+ assert(packet.reviewerTasks.includes("Route complete milestone packet to reviewers"));
+});
+
+test("builds a deterministic sponsor-facing progress summary", () => {
+ const audit = auditChallengeMilestones({ challenge, submissions, generatedAt });
+ const summary = buildProgressSummary(audit);
+
+ assert.deepStrictEqual(summary.counts, {
+ totalTeams: 2,
+ readyForReview: 1,
+ needsSolverAction: 0,
+ escalations: 1,
+ highRisk: 1,
+ });
+ assert.deepStrictEqual(summary.nextActions, [
+ "Resolve 1 escalation packet before arbitration.",
+ "Review 1 complete milestone packet.",
+ ]);
+});
+
+test("creates stable audit digests from packet facts", () => {
+ const audit = auditChallengeMilestones({ challenge, submissions, generatedAt });
+ const packet = audit.packets.find((item) => item.teamId === "team-aurora");
+
+ const first = createAuditDigest(packet);
+ const second = createAuditDigest({ ...packet, sponsorActions: [...packet.sponsorActions] });
+
+ assert.strictEqual(first, second);
+ assert.match(first, /^cmpm_[a-f0-9]{24}$/);
+});