Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions challenge-rubric-readiness-gate/README.md
Original file line number Diff line number Diff line change
@@ -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
```
16 changes: 16 additions & 0 deletions challenge-rubric-readiness-gate/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -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.
75 changes: 75 additions & 0 deletions challenge-rubric-readiness-gate/demo.js
Original file line number Diff line number Diff line change
@@ -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)}...`)
Binary file added challenge-rubric-readiness-gate/demo.mp4
Binary file not shown.
14 changes: 14 additions & 0 deletions challenge-rubric-readiness-gate/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
186 changes: 186 additions & 0 deletions challenge-rubric-readiness-gate/index.js
Original file line number Diff line number Diff line change
@@ -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,
}
16 changes: 16 additions & 0 deletions challenge-rubric-readiness-gate/requirements-map.md
Original file line number Diff line number Diff line change
@@ -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.
Loading