From b83b4c2c2e056ad66a14a559c48fc7810cd2aee4 Mon Sep 17 00:00:00 2001 From: tuanadr Date: Wed, 20 May 2026 17:16:02 +0700 Subject: [PATCH] Add challenge milestone progress monitor --- .../README.md | 44 +++ .../acceptance-notes.md | 27 ++ .../demo-output/audit-packets.json | 141 ++++++++++ .../demo-output/demo.mp4 | Bin 0 -> 43640 bytes .../demo-output/demo.svg | 61 +++++ challenge-milestone-progress-monitor/demo.js | 91 +++++++ challenge-milestone-progress-monitor/index.js | 256 ++++++++++++++++++ .../requirements-map.md | 16 ++ .../sample-data.js | 114 ++++++++ challenge-milestone-progress-monitor/test.js | 159 +++++++++++ 10 files changed, 909 insertions(+) create mode 100644 challenge-milestone-progress-monitor/README.md create mode 100644 challenge-milestone-progress-monitor/acceptance-notes.md create mode 100644 challenge-milestone-progress-monitor/demo-output/audit-packets.json create mode 100644 challenge-milestone-progress-monitor/demo-output/demo.mp4 create mode 100644 challenge-milestone-progress-monitor/demo-output/demo.svg create mode 100644 challenge-milestone-progress-monitor/demo.js create mode 100644 challenge-milestone-progress-monitor/index.js create mode 100644 challenge-milestone-progress-monitor/requirements-map.md create mode 100644 challenge-milestone-progress-monitor/sample-data.js create mode 100644 challenge-milestone-progress-monitor/test.js 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 0000000000000000000000000000000000000000..4fd9157dc0add00a0aa5ae6f91d79ae5d8edf697 GIT binary patch literal 43640 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 + + + 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}$/); +});