From 68b88c8152ba2a34141c144a861faa61e29d12aa Mon Sep 17 00:00:00 2001 From: Seowoo Han Date: Wed, 20 May 2026 19:27:27 +0900 Subject: [PATCH] Add challenge rubric readiness gate --- challenge-rubric-readiness-gate/README.md | 22 +++ .../acceptance-notes.md | 16 ++ challenge-rubric-readiness-gate/demo.js | 75 +++++++ challenge-rubric-readiness-gate/demo.mp4 | Bin 0 -> 37963 bytes challenge-rubric-readiness-gate/demo.svg | 14 ++ challenge-rubric-readiness-gate/index.js | 186 ++++++++++++++++++ .../requirements-map.md | 16 ++ challenge-rubric-readiness-gate/test.js | 121 ++++++++++++ 8 files changed, 450 insertions(+) create mode 100644 challenge-rubric-readiness-gate/README.md create mode 100644 challenge-rubric-readiness-gate/acceptance-notes.md create mode 100644 challenge-rubric-readiness-gate/demo.js create mode 100644 challenge-rubric-readiness-gate/demo.mp4 create mode 100644 challenge-rubric-readiness-gate/demo.svg create mode 100644 challenge-rubric-readiness-gate/index.js create mode 100644 challenge-rubric-readiness-gate/requirements-map.md create mode 100644 challenge-rubric-readiness-gate/test.js diff --git a/challenge-rubric-readiness-gate/README.md b/challenge-rubric-readiness-gate/README.md new file mode 100644 index 0000000..81aaa5e --- /dev/null +++ b/challenge-rubric-readiness-gate/README.md @@ -0,0 +1,22 @@ +# Challenge Rubric Readiness Gate + +This module covers a narrow Challenge Posting Portal slice of SCIBASE issue #18. + +It evaluates whether a scientific bounty is ready to publish before solvers spend time on it. The gate checks challenge context, deliverables, scoring rubric, timeline, prize reconciliation, payout triggers, private/NDA handling, submission privacy, and IP policy. + +## What It Does + +- Verifies required challenge posting fields. +- Ensures deliverables have acceptance evidence and unique IDs. +- Checks that rubric criteria are measurable and total 100 points. +- Confirms timeline dates are valid and chronological. +- Reconciles payout milestones against the prize amount. +- Flags NDA, prequalification, privacy, sponsor contact, and IP policy gaps. +- Emits readiness status, blockers, warnings, recommended actions, and an audit digest. + +## Run + +```bash +node challenge-rubric-readiness-gate/test.js +node challenge-rubric-readiness-gate/demo.js +``` diff --git a/challenge-rubric-readiness-gate/acceptance-notes.md b/challenge-rubric-readiness-gate/acceptance-notes.md new file mode 100644 index 0000000..2d86505 --- /dev/null +++ b/challenge-rubric-readiness-gate/acceptance-notes.md @@ -0,0 +1,16 @@ +# Acceptance Notes + +## Local Validation + +- `node challenge-rubric-readiness-gate/test.js` +- `node challenge-rubric-readiness-gate/demo.js` +- `node --check challenge-rubric-readiness-gate/index.js` +- `node --check challenge-rubric-readiness-gate/test.js` +- `node --check challenge-rubric-readiness-gate/demo.js` +- `ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 challenge-rubric-readiness-gate/demo.mp4` + +## Reviewer Notes + +- The module is dependency-free and uses synthetic challenge data only. +- It does not handle live payments, private sponsor data, or solver identity data. +- It is intentionally scoped to pre-posting challenge readiness, not submission review or reward distribution. diff --git a/challenge-rubric-readiness-gate/demo.js b/challenge-rubric-readiness-gate/demo.js new file mode 100644 index 0000000..1c92e96 --- /dev/null +++ b/challenge-rubric-readiness-gate/demo.js @@ -0,0 +1,75 @@ +"use strict" + +const { evaluateChallengePosting } = require("./index") + +const result = evaluateChallengePosting({ + id: "challenge-biomarker-2026", + title: "Single-cell biomarker discovery for treatment response", + scientificContext: + "Sponsors have collected single-cell RNA-seq profiles across matched responder and non-responder cohorts. The posted challenge asks solvers to identify reproducible biomarker signatures while preserving patient privacy and making all model claims auditable.", + problemStatement: + "Build and validate a biomarker-ranking workflow that separates responder and non-responder samples, explains candidate pathways, and produces a reproducible report with evidence-linked outputs.", + deliverables: [ + { + id: "model", + name: "Ranking model", + fileFormat: "notebook + JSON", + acceptanceEvidence: "Top-ranked markers include model score, pathway note, and holdout validation metric.", + }, + { + id: "report", + name: "Scientific report", + fileFormat: "PDF", + acceptanceEvidence: "Report includes methods, validation, limitations, and reproducibility instructions.", + }, + ], + evaluationRubric: [ + { + name: "Scientific validity", + weight: 40, + deliverableId: "model", + measurement: "Holdout AUC, pathway plausibility, and leakage checks are reviewed together.", + }, + { + name: "Reproducibility", + weight: 30, + deliverableId: "report", + measurement: "Reviewer can rerun the workflow from the submitted package.", + }, + { + name: "Communication", + weight: 30, + deliverableId: "report", + measurement: "Findings, limitations, and sponsor next steps are understandable to domain reviewers.", + }, + ], + timeline: { + openAt: "2026-06-01", + submissionDue: "2026-07-01", + reviewDue: "2026-07-15", + awardDue: "2026-07-22", + }, + prize: { amount: 5000, currency: "USD" }, + payoutMilestones: [ + { amount: 1000, trigger: "proposal shortlist" }, + { amount: 4000, trigger: "final award" }, + ], + privateChallenge: true, + requiresNda: true, + ndaPlan: "Platform NDA before data-room access", + prequalification: "Solvers submit prior bioinformatics work and privacy acknowledgement", + sponsorContact: { role: "R&D program manager" }, + ipPolicy: { defaultLicense: "solver retains IP until paid" }, + submissionPrivacy: { visibility: "private to sponsor and reviewers" }, +}) + +console.log("Challenge Rubric Readiness Gate Demo") +console.log("====================================") +console.log(`title: ${result.title}`) +console.log(`status: ${result.status}`) +console.log(`readiness score: ${result.readinessScore}`) +console.log(`deliverables: ${result.deliverableCount}`) +console.log(`rubric total: ${result.rubricWeightTotal}`) +console.log(`payout total: ${result.payoutTotal}`) +console.log(`top action: ${result.actions[0]}`) +console.log(`digest: ${result.auditDigest.slice(0, 16)}...`) diff --git a/challenge-rubric-readiness-gate/demo.mp4 b/challenge-rubric-readiness-gate/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..680b66409070145913cca18e0e014dd71e06a05f GIT binary patch literal 37963 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 z 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 NF3o`IlJ!|z{x5H|XTbmf literal 0 HcmV?d00001 diff --git a/challenge-rubric-readiness-gate/demo.svg b/challenge-rubric-readiness-gate/demo.svg new file mode 100644 index 0000000..076619e --- /dev/null +++ b/challenge-rubric-readiness-gate/demo.svg @@ -0,0 +1,14 @@ + + + + Challenge Rubric Readiness Gate + Pre-posting checks for scientific bounty challenges + + READY + rubric 100 / prize reconciled / timeline chronological + Checks + deliverables include acceptance evidence + rubric criteria are measurable and mapped + private challenge has NDA and prequalification path + audit digest attaches to sponsor review + diff --git a/challenge-rubric-readiness-gate/index.js b/challenge-rubric-readiness-gate/index.js new file mode 100644 index 0000000..e95d154 --- /dev/null +++ b/challenge-rubric-readiness-gate/index.js @@ -0,0 +1,186 @@ +"use strict" + +const crypto = require("node:crypto") + +const REQUIRED_FIELDS = [ + "title", + "scientificContext", + "problemStatement", + "deliverables", + "evaluationRubric", + "timeline", + "prize", +] + +const REQUIRED_TIMELINE_KEYS = ["openAt", "submissionDue", "reviewDue", "awardDue"] + +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]` + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}` + } + return JSON.stringify(value) +} + +function digest(value) { + return crypto.createHash("sha256").update(stableStringify(value)).digest("hex") +} + +function hasText(value, minLength = 1) { + return typeof value === "string" && value.trim().length >= minLength +} + +function asDate(value) { + const date = new Date(value) + return Number.isNaN(date.getTime()) ? null : date +} + +function unique(values) { + return [...new Set(values)] +} + +function evaluateChallengePosting(input) { + const challenge = input || {} + const blockers = [] + const warnings = [] + const actions = [] + const evidence = [] + + for (const field of REQUIRED_FIELDS) { + const value = challenge[field] + if (Array.isArray(value) ? value.length === 0 : value == null || value === "") { + blockers.push(`missing required field: ${field}`) + } + } + + if (!hasText(challenge.scientificContext, 120)) { + warnings.push("scientific context is too thin for external solvers") + } else { + evidence.push("scientific context is solver-readable") + } + + if (!hasText(challenge.problemStatement, 80)) { + blockers.push("problem statement needs a concrete scientific task") + } else { + evidence.push("problem statement describes a concrete task") + } + + const deliverables = challenge.deliverables || [] + const deliverableIds = deliverables.map((item) => item.id).filter(Boolean) + for (const deliverable of deliverables) { + if (!hasText(deliverable.name)) blockers.push(`deliverable ${deliverable.id || "unknown"} has no name`) + if (!hasText(deliverable.acceptanceEvidence)) { + blockers.push(`deliverable ${deliverable.id || deliverable.name || "unknown"} lacks acceptance evidence`) + } + if (!hasText(deliverable.fileFormat)) { + warnings.push(`deliverable ${deliverable.id || deliverable.name || "unknown"} has no expected file format`) + } + } + if (deliverableIds.length !== unique(deliverableIds).length) { + blockers.push("deliverable IDs must be unique") + } + + const rubric = challenge.evaluationRubric || [] + const totalWeight = rubric.reduce((sum, criterion) => sum + Number(criterion.weight || 0), 0) + if (rubric.length < 3) blockers.push("evaluation rubric needs at least three criteria") + if (totalWeight !== 100) blockers.push(`evaluation rubric weights must total 100, got ${totalWeight}`) + for (const criterion of rubric) { + if (!hasText(criterion.name)) blockers.push("rubric criterion is missing a name") + if (!hasText(criterion.measurement)) { + blockers.push(`rubric criterion ${criterion.name || "unknown"} lacks a measurable standard`) + } + if (criterion.deliverableId && !deliverableIds.includes(criterion.deliverableId)) { + blockers.push(`rubric criterion ${criterion.name || "unknown"} references an unknown deliverable`) + } + } + + const timeline = challenge.timeline || {} + const timelineDates = REQUIRED_TIMELINE_KEYS.map((key) => [key, asDate(timeline[key])]) + for (const [key, date] of timelineDates) { + if (!date) blockers.push(`timeline ${key} is missing or invalid`) + } + const validDates = timelineDates.every(([, date]) => date) + if (validDates) { + for (let i = 1; i < timelineDates.length; i += 1) { + const [previousKey, previousDate] = timelineDates[i - 1] + const [currentKey, currentDate] = timelineDates[i] + if (currentDate <= previousDate) { + blockers.push(`timeline ${currentKey} must be after ${previousKey}`) + } + } + evidence.push("timeline is chronological") + } + + const prize = challenge.prize || {} + const prizeAmount = Number(prize.amount || 0) + if (!Number.isFinite(prizeAmount) || prizeAmount <= 0) blockers.push("prize amount must be positive") + if (!hasText(prize.currency)) blockers.push("prize currency is required") + + const payouts = challenge.payoutMilestones || [] + const payoutTotal = payouts.reduce((sum, payout) => sum + Number(payout.amount || 0), 0) + if (payouts.length === 0) { + blockers.push("payout milestones are required") + } else if (payoutTotal !== prizeAmount) { + blockers.push(`payout milestones must total prize amount ${prizeAmount}, got ${payoutTotal}`) + } else { + evidence.push("payout milestones reconcile to the prize amount") + } + for (const payout of payouts) { + if (!hasText(payout.trigger)) blockers.push("payout milestone is missing a trigger") + } + + const sensitive = Boolean(challenge.privateChallenge || challenge.requiresNda) + if (sensitive && !challenge.ndaPlan) blockers.push("private or NDA challenge needs an NDA plan") + if (sensitive && !challenge.prequalification) { + warnings.push("sensitive challenge should define prequalification criteria") + } + + if (!challenge.sponsorContact || !hasText(challenge.sponsorContact.role)) { + warnings.push("sponsor contact role is missing") + } + if (!challenge.ipPolicy || !hasText(challenge.ipPolicy.defaultLicense)) { + warnings.push("IP/license policy should be explicit before posting") + } + if (!challenge.submissionPrivacy || !hasText(challenge.submissionPrivacy.visibility)) { + warnings.push("submission visibility is not specified") + } + + if (blockers.length > 0) { + actions.push("Resolve blockers before accepting the challenge posting.") + } + if (warnings.length > 0) { + actions.push("Tighten warning items before sponsor approval.") + } + actions.push("Attach this readiness packet to the challenge posting review.") + + const possibleChecks = 16 + const penalty = blockers.length * 3 + warnings.length + const readinessScore = Math.max(0, Math.min(100, Math.round(((possibleChecks * 3 - penalty) / (possibleChecks * 3)) * 100))) + const status = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "needs-review" : "ready" + + const result = { + challengeId: challenge.id || null, + title: challenge.title || "Untitled challenge", + status, + readinessScore, + blockers, + warnings, + evidence, + actions, + rubricWeightTotal: totalWeight, + payoutTotal, + deliverableCount: deliverables.length, + } + + return { + ...result, + auditDigest: digest(result), + } +} + +module.exports = { + evaluateChallengePosting, +} diff --git a/challenge-rubric-readiness-gate/requirements-map.md b/challenge-rubric-readiness-gate/requirements-map.md new file mode 100644 index 0000000..256d756 --- /dev/null +++ b/challenge-rubric-readiness-gate/requirements-map.md @@ -0,0 +1,16 @@ +# Requirements Map + +| Issue #18 requirement | Coverage in this module | +| --- | --- | +| Challenge Posting Portal | Validates whether a proposed challenge can be published safely. | +| Problem description and scientific context | Requires concrete scientific context and problem statement text. | +| Deliverables | Checks named deliverables, file formats, unique IDs, and acceptance evidence. | +| Evaluation criteria and scoring rubric | Requires measurable rubric criteria that total 100 points and map to deliverables. | +| Timeline and milestone deadlines | Validates chronological open, submission, review, and award dates. | +| Prize amount and payout schedule | Reconciles payout milestones against the advertised prize amount. | +| Public vs. private challenges and NDA support | Blocks private/NDA challenges without an NDA plan and warns on missing prequalification. | +| Trust between sponsors and solvers | Produces a deterministic audit digest and action packet before posting. | + +## Non-Overlap Note + +This submission is distinct from broad bounty system modules, submission package builders, payout eligibility gates, IP redaction gates, amendment controls, appeals ledgers, sponsor scorecards, collusion checks, and milestone monitors. It focuses specifically on the pre-posting readiness gate for challenge descriptions, deliverables, rubrics, timelines, and payout schedules. diff --git a/challenge-rubric-readiness-gate/test.js b/challenge-rubric-readiness-gate/test.js new file mode 100644 index 0000000..cd67c32 --- /dev/null +++ b/challenge-rubric-readiness-gate/test.js @@ -0,0 +1,121 @@ +"use strict" + +const assert = require("node:assert/strict") +const { evaluateChallengePosting } = require("./index") + +const readyChallenge = { + id: "challenge-biomarker-2026", + title: "Single-cell biomarker discovery for treatment response", + scientificContext: + "Sponsors have collected single-cell RNA-seq profiles across matched responder and non-responder cohorts. The posted challenge asks solvers to identify reproducible biomarker signatures while preserving patient privacy and making all model claims auditable.", + problemStatement: + "Build and validate a biomarker-ranking workflow that separates responder and non-responder samples, explains candidate pathways, and produces a reproducible report with evidence-linked outputs.", + deliverables: [ + { + id: "model", + name: "Ranking model", + fileFormat: "notebook + JSON", + acceptanceEvidence: "Top-ranked markers must include model score, pathway note, and holdout validation metric.", + }, + { + id: "report", + name: "Scientific report", + fileFormat: "PDF", + acceptanceEvidence: "Report must include methods, validation, limitations, and reproducibility instructions.", + }, + ], + evaluationRubric: [ + { + name: "Scientific validity", + weight: 40, + deliverableId: "model", + measurement: "Holdout AUC, pathway plausibility, and leakage checks are reviewed together.", + }, + { + name: "Reproducibility", + weight: 30, + deliverableId: "report", + measurement: "Reviewer can rerun the workflow from the submitted package.", + }, + { + name: "Communication", + weight: 30, + deliverableId: "report", + measurement: "Findings, limitations, and sponsor next steps are understandable to domain reviewers.", + }, + ], + timeline: { + openAt: "2026-06-01", + submissionDue: "2026-07-01", + reviewDue: "2026-07-15", + awardDue: "2026-07-22", + }, + prize: { amount: 5000, currency: "USD" }, + payoutMilestones: [ + { amount: 1000, trigger: "proposal shortlist" }, + { amount: 4000, trigger: "final award" }, + ], + privateChallenge: true, + requiresNda: true, + ndaPlan: "Platform NDA before data-room access", + prequalification: "Solvers submit prior bioinformatics work and privacy acknowledgement", + sponsorContact: { role: "R&D program manager" }, + ipPolicy: { defaultLicense: "solver retains IP until paid" }, + submissionPrivacy: { visibility: "private to sponsor and reviewers" }, +} + +{ + const result = evaluateChallengePosting(readyChallenge) + assert.equal(result.status, "ready") + assert.equal(result.rubricWeightTotal, 100) + assert.equal(result.payoutTotal, 5000) + assert.equal(result.deliverableCount, 2) + assert.match(result.auditDigest, /^[0-9a-f]{64}$/) +} + +{ + const result = evaluateChallengePosting({ + ...readyChallenge, + evaluationRubric: readyChallenge.evaluationRubric.map((criterion) => ({ + ...criterion, + weight: criterion.name === "Communication" ? 10 : criterion.weight, + })), + payoutMilestones: [{ amount: 2500, trigger: "final award" }], + }) + + assert.equal(result.status, "blocked") + assert.ok(result.blockers.includes("evaluation rubric weights must total 100, got 80")) + assert.ok(result.blockers.includes("payout milestones must total prize amount 5000, got 2500")) +} + +{ + const result = evaluateChallengePosting({ + ...readyChallenge, + deliverables: [ + { + id: "model", + name: "Ranking model", + fileFormat: "notebook", + acceptanceEvidence: "Ranking output is present.", + }, + { + id: "model", + name: "Duplicate model", + fileFormat: "JSON", + acceptanceEvidence: "", + }, + ], + privateChallenge: true, + requiresNda: true, + ndaPlan: null, + prequalification: "", + }) + + assert.equal(result.status, "blocked") + assert.ok(result.blockers.includes("deliverable model lacks acceptance evidence")) + assert.ok(result.blockers.includes("deliverable IDs must be unique")) + assert.ok(result.blockers.includes("private or NDA challenge needs an NDA plan")) + assert.ok(result.warnings.includes("sensitive challenge should define prequalification criteria")) +} + +console.log("challenge-rubric-readiness-gate tests passed")