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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# deepevents.ai
deepevents.ai main codebase

## Scientific bounty additions

- `scientific-bounty-amendment-control`: sponsor amendment control ledger for scientific bounty changes after solvers start, including impact findings, solver notification packets, evaluation holds, and signed audit evidence.
29 changes: 29 additions & 0 deletions scientific-bounty-amendment-control/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Scientific Bounty Amendment Control

This module implements a focused milestone for SCIBASE issue #18, the Scientific Bounty System. It models sponsor-side challenge amendments after solvers have started, then produces deterministic safety evidence for reviewer and admin workflows.

The slice is intentionally narrow: it does not duplicate challenge intake, scoring, arbitration, escrow settlement, appeals, workspace privacy, anti-collusion, or reproducibility-audit submissions. It covers the change-control gap between a published challenge and final evaluation.

## What It Does

- Detects material amendments after registrations, submissions, or reviewer assignments begin.
- Classifies deadline, prize, rubric, deliverable, NDA, IP policy, and visibility changes by severity.
- Builds solver notification packets with acknowledgement state and notice-window coverage.
- Places evaluation, reviewer assignment, or payout-readiness holds when fairness rules are not met.
- Emits signed audit events and a deterministic digest for sponsor, solver, and moderator review.

## Run Locally

```bash
npm run check
npm test
npm run demo
npm run demo:gif
```

## Demo Artifacts

- `docs/demo.svg` is a static dashboard preview.
- `docs/demo.gif` is generated by `npm run demo:gif` and shows three review states.

The implementation uses synthetic data only. It does not connect to payment rails, accept platform terms, or perform any live payout or identity action.
115 changes: 115 additions & 0 deletions scientific-bounty-amendment-control/data/sample-amendments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"generatedAt": "2026-05-16T15:05:00.000Z",
"challenge": {
"id": "challenge-bio-marker-042",
"title": "Single-cell biomarker discovery challenge",
"status": "published",
"registrationOpenedAt": "2026-05-01T09:00:00.000Z",
"submissionOpenedAt": "2026-05-10T09:00:00.000Z",
"submissionDueAt": "2026-05-28T23:59:00.000Z",
"reviewStartsAt": "2026-05-29T12:00:00.000Z",
"prizePoolUsd": 42000,
"ipPolicy": "solver_retains_until_paid",
"visibility": "named_finalists",
"requiredDeliverables": ["model-card", "reproducible-notebook", "ranked-biomarkers", "license-attestation"],
"rubric": {
"accuracy": 40,
"reproducibility": 25,
"biological-plausibility": 20,
"documentation": 15
}
},
"teams": [
{
"id": "team-atlas",
"name": "Atlas Cell Lab",
"registeredAt": "2026-05-02T12:00:00.000Z",
"submissionStartedAt": "2026-05-11T18:20:00.000Z",
"lastActiveAt": "2026-05-15T17:30:00.000Z"
},
{
"id": "team-orchid",
"name": "Orchid Systems Biology",
"registeredAt": "2026-05-04T16:10:00.000Z",
"submissionStartedAt": "2026-05-12T20:00:00.000Z",
"lastActiveAt": "2026-05-14T19:15:00.000Z"
},
{
"id": "team-quartz",
"name": "Quartz ML Group",
"registeredAt": "2026-05-08T08:40:00.000Z",
"submissionStartedAt": null,
"lastActiveAt": "2026-05-15T10:00:00.000Z"
}
],
"reviewers": [
{
"id": "reviewer-chen",
"assignedAt": "2026-05-15T12:00:00.000Z"
},
{
"id": "reviewer-mbaye",
"assignedAt": "2026-05-15T12:05:00.000Z"
}
],
"amendments": [
{
"id": "amd-deadline-shorter",
"requestedBy": "sponsor",
"requestedAt": "2026-05-16T11:00:00.000Z",
"effectiveAt": "2026-05-18T09:00:00.000Z",
"field": "submissionDueAt",
"before": "2026-05-28T23:59:00.000Z",
"after": "2026-05-22T23:59:00.000Z",
"justification": "Sponsor board wants winners before June budget close.",
"notifiedTeamIds": ["team-atlas"],
"acknowledgedTeamIds": []
},
{
"id": "amd-rubric-shift",
"requestedBy": "sponsor",
"requestedAt": "2026-05-16T11:07:00.000Z",
"effectiveAt": "2026-05-20T09:00:00.000Z",
"field": "rubric",
"before": {
"accuracy": 40,
"reproducibility": 25,
"biological-plausibility": 20,
"documentation": 15
},
"after": {
"accuracy": 30,
"reproducibility": 20,
"biological-plausibility": 40,
"documentation": 10
},
"justification": "Sponsor wants to emphasize biological plausibility.",
"notifiedTeamIds": ["team-atlas", "team-orchid", "team-quartz"],
"acknowledgedTeamIds": ["team-quartz"]
},
{
"id": "amd-ip-policy",
"requestedBy": "sponsor",
"requestedAt": "2026-05-16T12:00:00.000Z",
"effectiveAt": "2026-05-17T09:00:00.000Z",
"field": "ipPolicy",
"before": "solver_retains_until_paid",
"after": "sponsor_option_license_pre_award",
"justification": "Sponsor legal team requested earlier internal review rights.",
"notifiedTeamIds": [],
"acknowledgedTeamIds": []
},
{
"id": "amd-prize-increase",
"requestedBy": "sponsor",
"requestedAt": "2026-05-16T12:20:00.000Z",
"effectiveAt": "2026-05-16T13:00:00.000Z",
"field": "prizePoolUsd",
"before": 42000,
"after": 48000,
"justification": "Additional honorable mention funding approved.",
"notifiedTeamIds": ["team-atlas", "team-orchid", "team-quartz"],
"acknowledgedTeamIds": ["team-atlas", "team-orchid", "team-quartz"]
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
34 changes: 34 additions & 0 deletions scientific-bounty-amendment-control/docs/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions scientific-bounty-amendment-control/docs/requirement-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Requirement Map

This module maps to SCIBASE issue #18, Scientific Bounty System.

| Issue requirement | Implementation |
| --- | --- |
| Challenge posting portal | Validates sponsor amendments against published challenge terms, including deliverables, rubrics, prize amounts, timelines, prequalification rules, NDA posture, and IP policy. |
| Timeline and milestone deadlines | Flags shortened submission windows, review-start changes, short notice windows, and missing solver acknowledgement before changes take effect. |
| Prize amount and payout schedule | Detects prize decreases or increases, blocks payout readiness when material changes are not acknowledged, and emits signed hold decisions. |
| Evaluation criteria and scoring rubric | Computes rubric criterion shifts and freezes evaluation when rubric changes after submissions or reviewer assignments. |
| Public vs. private / anonymous participation | Treats visibility, NDA, prequalification, and IP policy changes as participation-term amendments requiring solver notice and acknowledgement. |
| Submission engine | Uses registered teams, active submissions, and reviewer assignments to decide who is affected by a sponsor amendment. |
| Arbitration and reward distribution | Places evaluation, reviewer-assignment, and payout-readiness holds when challenge terms change in a way that could unfairly affect solvers. |
| Audit logs for reproducibility | Generates deterministic finding IDs, solver-notification IDs, audit event IDs, and a final evidence digest. |

## Non-overlap

This is a challenge amendment control layer. It does not implement a general challenge intake system, solver workspace, scoring engine, arbitration module, appeals ledger, escrow settlement module, anti-collusion detector, or reproducibility audit gate. It focuses on the sponsor-change interval after a bounty is already live and before evaluation or payout readiness proceeds.
12 changes: 12 additions & 0 deletions scientific-bounty-amendment-control/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "scientific-bounty-amendment-control",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"check": "node --check src/challenge-amendment-control.js && node --check scripts/demo.js && node --check scripts/write-demo-gif.js && node --check test/challenge-amendment-control.test.js",
"test": "node --test test/challenge-amendment-control.test.js",
"demo": "node scripts/demo.js",
"demo:gif": "node scripts/write-demo-gif.js"
}
}
18 changes: 18 additions & 0 deletions scientific-bounty-amendment-control/scripts/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { evaluateChallengeAmendments } from "../src/challenge-amendment-control.js";

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const sample = JSON.parse(readFileSync(join(root, "data", "sample-amendments.json"), "utf8"));
const report = evaluateChallengeAmendments(sample);

console.log(JSON.stringify({
challenge: report.challengeId,
digest: report.evidenceDigest,
summary: report.summary,
criticalAmendments: report.findings
.filter((finding) => finding.severity === "critical")
.map((finding) => ({ amendmentId: finding.amendmentId, field: finding.details.field, direction: finding.details.direction })),
holdDecisions: report.holdDecisions
}, null, 2));
118 changes: 118 additions & 0 deletions scientific-bounty-amendment-control/scripts/write-demo-gif.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const out = join(root, "docs", "demo.gif");

function u16(value) {
return String.fromCharCode(value & 255, (value >> 8) & 255);
}

function color(r, g, b) {
return String.fromCharCode(r, g, b);
}

function minCodeSize(colorCount) {
return Math.max(2, Math.ceil(Math.log2(colorCount)));
}

function packCodes(codes, width) {
let bits = 0;
let bitCount = 0;
const bytes = [];
for (const code of codes) {
bits |= code << bitCount;
bitCount += width;
while (bitCount >= 8) {
bytes.push(bits & 255);
bits >>= 8;
bitCount -= 8;
}
}
if (bitCount > 0) bytes.push(bits & 255);
return bytes;
}

function imageData(indices, paletteSize) {
const size = minCodeSize(paletteSize);
const clear = 1 << size;
const end = clear + 1;
const width = size + 1;
const codes = [clear];
for (let index = 0; index < indices.length; index += 4) {
if (index > 0) codes.push(clear);
codes.push(...indices.slice(index, index + 4));
}
codes.push(end);
const bytes = packCodes(codes, width);
const blocks = [];
for (let index = 0; index < bytes.length; index += 255) {
const chunk = bytes.slice(index, index + 255);
blocks.push(String.fromCharCode(chunk.length, ...chunk));
}
return String.fromCharCode(size) + blocks.join("") + "\x00";
}

function frame(width, height, indices, delayCs, paletteSize) {
return [
"\x21\xF9\x04\x04",
u16(delayCs),
"\x00\x00",
"\x2C",
u16(0),
u16(0),
u16(width),
u16(height),
"\x00",
imageData(indices, paletteSize)
].join("");
}

function makeFrame(width, height, accentIndex) {
const pixels = new Array(width * height).fill(0);
const fill = (x0, y0, x1, y1, idx) => {
for (let y = y0; y < y1; y += 1) {
for (let x = x0; x < x1; x += 1) pixels[y * width + x] = idx;
}
};

fill(0, 0, width, Math.floor(height * 0.14), 1);
fill(Math.floor(width * 0.03), Math.floor(height * 0.24), Math.floor(width * 0.32), Math.floor(height * 0.43), 2);
fill(Math.floor(width * 0.35), Math.floor(height * 0.24), Math.floor(width * 0.65), Math.floor(height * 0.43), 2);
fill(Math.floor(width * 0.68), Math.floor(height * 0.24), Math.floor(width * 0.97), Math.floor(height * 0.43), 2);
fill(Math.floor(width * 0.06), Math.floor(height * 0.63), Math.floor(width * 0.31), Math.floor(height * 0.72), accentIndex);
fill(Math.floor(width * 0.38), Math.floor(height * 0.63), Math.floor(width * 0.63), Math.floor(height * 0.72), 4);
fill(Math.floor(width * 0.69), Math.floor(height * 0.63), Math.floor(width * 0.94), Math.floor(height * 0.72), 5);
fill(Math.floor(width * 0.08), Math.floor(height * 0.83), Math.floor(width * 0.92), Math.floor(height * 0.89), accentIndex);
return pixels;
}

const width = 960;
const height = 540;
const palette = [
color(246, 247, 251),
color(17, 24, 39),
color(255, 255, 255),
color(185, 28, 28),
color(37, 99, 235),
color(180, 83, 9),
color(22, 163, 74),
color(209, 213, 219)
].join("");

const gif = [
"GIF89a",
u16(width),
u16(height),
"\xF2\x00\x00",
palette,
"\x21\xFF\x0BNETSCAPE2.0\x03\x01\x00\x00\x00",
frame(width, height, makeFrame(width, height, 3), 80, 8),
frame(width, height, makeFrame(width, height, 5), 80, 8),
frame(width, height, makeFrame(width, height, 6), 80, 8),
";"
].join("");

writeFileSync(out, Buffer.from(gif, "binary"));
console.log(`wrote ${out}`);
Loading