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 @@ + + + + Challenge Milestone Progress Monitor + Biomarker discovery challenge - 2026-05-20T12:00:00.000Z + + + 3 + Teams + + + + 1 + Ready + + + + 1 + Escalations + + + + 1 + Solver action + + + + + Aurora Lab + Prototype package - Hold For Escalation + Risk 13 + MISSING_MILESTONE_EVIDENCE | SPONSOR_FEEDBACK_OVERDUE | SOLVER_IDLE_RISK | RESUBMISSION_WINDOW_EXPIRED | REVIEW_PACKET_INCOMPLETE + cmpm_878fb4e07adaa84b4cae08c1 + + + + Nova Institute + Prototype package - Ready For Review + Risk 0 + No flags + cmpm_8d4e2d1f634b250de91e464d + + + + Quasar Students + Final replication packet - Needs Solver Action + Risk 5 + MISSING_MILESTONE_EVIDENCE | REVIEW_PACKET_INCOMPLETE + cmpm_6ddb98999ede6078045762d1 + + Resolve 1 escalation packet before arbitration. Request solver updates for 1 milestone packet. Review 1 complete milestone packet. + \ 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 ` + + + Challenge Milestone Progress Monitor + ${escapeXml(audit.title)} - ${escapeXml(audit.generatedAt)} + ${metricCard(64, 112, "Teams", audit.summary.counts.totalTeams, "#0b5fff")} + ${metricCard(252, 112, "Ready", audit.summary.counts.readyForReview, "#1f8a5b")} + ${metricCard(440, 112, "Escalations", audit.summary.counts.escalations, "#b42318")} + ${metricCard(628, 112, "Solver action", audit.summary.counts.needsSolverAction, "#ad6f00")} + ${rows} + ${escapeXml(audit.summary.nextActions.join(" "))} +`; +} + +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}$/); +});