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
24 changes: 24 additions & 0 deletions challenge-amendment-consent-ledger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Challenge Amendment Consent Ledger

This submission targets [SCIBASE issue #18](https://github.com/SCIBASE-AI/SCIBASE.AI/issues/18) with a focused Scientific Bounty System module.

It handles sponsor-side change control after a scientific bounty is already live. When a sponsor changes deliverables, rubrics, deadlines, payout schedules, private-data requirements, or IP terms, the ledger classifies the amendment, locks pre-change submission evidence, requires solver re-consent, and produces an arbitration-ready packet.

## What It Adds

- Materiality scoring for bounty amendments.
- Solver re-consent decisions for changed deliverables, criteria, deadlines, payout terms, IP terms, and private-data requirements.
- Locked evidence packets for submissions made before the amendment.
- Protected withdrawal detection when a solver already submitted work before a material scope change.
- Arbitration packet output for payout holds, safe-to-continue teams, and amendment digests.

## Demo

```powershell
node challenge-amendment-consent-ledger/test.js
node challenge-amendment-consent-ledger/demo.js
```

`demo.mp4` is the reviewer-facing video artifact for the bounty submission. It walks through the problem, implementation, acceptance path, and command validation in 8.4 seconds. `demo.svg` provides a static workflow diagram.

See `acceptance-notes.md` for the payout-gate evidence checklist.
31 changes: 31 additions & 0 deletions challenge-amendment-consent-ledger/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Acceptance Notes

This is a focused implementation for SCIBASE issue #18, not a generic AI-generated content drop. The slice targets a specific marketplace trust problem that was not covered by the repeated broad intake, scoring, arbitration, escrow, or payout-ledger submissions already in the issue thread.

## What Changed

- Added materiality scoring for sponsor amendments after a bounty is live.
- Added solver re-consent decisions for changed deliverables, criteria, deadlines, payout terms, IP terms, and private-data requirements.
- Added locked evidence packets for submissions that existed before a material change.
- Added protected-withdrawal and payout-hold states for arbitration.
- Added focused dependency-free tests and demo data.

## Video Demo

- `demo.mp4` shows the problem, implementation, acceptance behavior, and validation command.
- `demo.svg` provides a static workflow diagram.

## Validation

Run from the repository root:

```powershell
node challenge-amendment-consent-ledger/test.js
node challenge-amendment-consent-ledger/demo.js
```

Expected result: the test prints `challenge-amendment-consent-ledger tests passed`, and the demo prints active material amendments, protected withdrawal teams, blocked award teams, and an arbitration packet digest.

## Integration Notes

The module is dependency-free and uses plain challenge, amendment, team, and submission objects. The next integration step is wiring those objects to the SCIBASE challenge posting portal and sponsor amendment workflow.
48 changes: 48 additions & 0 deletions challenge-amendment-consent-ledger/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use strict";

const { buildAmendmentLedger } = require("./index");

const ledger = buildAmendmentLedger({
challenge: {
id: "climate-forecasting-prize",
title: "Regional climate forecast benchmark",
finalDeadline: "2026-07-31T23:59:59Z",
payoutSchedule: "milestone_40_final_60",
ipTerms: "solver_retains_until_paid",
privateDataRequired: false
},
amendments: [
{
id: "amend-rubric-v2",
sponsorId: "climate-nonprofit",
requestedAt: "2026-07-14T12:00:00Z",
reason: "added wildfire smoke impact validation",
changes: {
deliverablesAdded: ["wildfire-smoke-validation-notebook"],
evaluationCriteriaAdded: ["smoke-event regional accuracy"],
rubricWeightDelta: 18,
deadlineMovedTo: "2026-07-24T23:59:59Z"
}
}
],
teams: [
{ teamId: "open-climate-lab", consentedAmendmentIds: ["amend-rubric-v2"] },
{ teamId: "student-forecast-team", consentedAmendmentIds: [] }
],
submissions: [
{
id: "forecast-v1",
teamId: "student-forecast-team",
submittedAt: "2026-07-10T09:30:00Z",
artifactHashes: ["model:9d2a", "report:af18", "notebook:813c"]
}
]
});

console.log(JSON.stringify({
activeMaterialAmendments: ledger.arbitrationPacket.activeMaterialAmendmentIds,
safeToContinueTeamIds: ledger.arbitrationPacket.safeToContinueTeamIds,
protectedWithdrawalTeamIds: ledger.arbitrationPacket.protectedWithdrawalTeamIds,
blockedAwardTeamIds: ledger.arbitrationPacket.blockedAwardTeamIds,
packetDigest: ledger.arbitrationPacket.packetDigest
}, null, 2));
Binary file added challenge-amendment-consent-ledger/demo.mp4
Binary file not shown.
27 changes: 27 additions & 0 deletions challenge-amendment-consent-ledger/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
220 changes: 220 additions & 0 deletions challenge-amendment-consent-ledger/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
"use strict";

const crypto = require("crypto");

function stableJson(value) {
if (Array.isArray(value)) {
return value.map(stableJson);
}

if (value && typeof value === "object") {
return Object.keys(value)
.sort()
.reduce((sorted, key) => {
sorted[key] = stableJson(value[key]);
return sorted;
}, {});
}

return value;
}

function digest(value) {
return crypto
.createHash("sha256")
.update(JSON.stringify(stableJson(value)))
.digest("hex");
}

function asArray(value) {
return Array.isArray(value) ? value : [];
}

function daysBetween(start, end) {
const startTime = Date.parse(start);
const endTime = Date.parse(end);
if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) return 0;
return Math.round((endTime - startTime) / 86400000);
}

function classifyAmendment(amendment, challenge) {
const changes = amendment.changes || {};
const materialChanges = [];
let materialityScore = 0;

if (asArray(changes.deliverablesAdded).length > 0) {
materialChanges.push("deliverables_added");
materialityScore += changes.deliverablesAdded.length * 20;
}

if (asArray(changes.evaluationCriteriaAdded).length > 0) {
materialChanges.push("criteria_added");
materialityScore += changes.evaluationCriteriaAdded.length * 16;
}

if (changes.rubricWeightDelta && Math.abs(changes.rubricWeightDelta) >= 10) {
materialChanges.push("rubric_weight_shift");
materialityScore += Math.min(30, Math.abs(changes.rubricWeightDelta));
}

if (changes.deadlineMovedTo) {
const shiftDays = daysBetween(challenge.finalDeadline, changes.deadlineMovedTo);
if (shiftDays < 0) {
materialChanges.push("deadline_shortened");
materialityScore += Math.min(25, Math.abs(shiftDays) * 3);
}
}

if (changes.payoutSchedule && changes.payoutSchedule !== challenge.payoutSchedule) {
materialChanges.push("payout_schedule_changed");
materialityScore += 24;
}

if (changes.ipTerms && changes.ipTerms !== challenge.ipTerms) {
materialChanges.push("ip_terms_changed");
materialityScore += 30;
}

if (changes.privateDataRequired === true && challenge.privateDataRequired !== true) {
materialChanges.push("private_data_added");
materialityScore += 32;
}

const requiresReconsent = materialityScore >= 20;

return {
id: amendment.id,
sponsorId: amendment.sponsorId,
requestedAt: amendment.requestedAt,
reason: amendment.reason || "not provided",
materialChanges,
materialityScore,
requiresReconsent,
digest: digest({
challengeId: challenge.id,
amendmentId: amendment.id,
changes,
requestedAt: amendment.requestedAt
})
};
}

function lockSubmissionEvidence(team, submissions, amendment) {
const lockedSubmissions = submissions
.filter((submission) => {
return submission.teamId === team.teamId &&
Date.parse(submission.submittedAt) <= Date.parse(amendment.requestedAt);
})
.map((submission) => ({
id: submission.id,
submittedAt: submission.submittedAt,
artifactHashes: asArray(submission.artifactHashes).slice().sort(),
manifestDigest: digest({
id: submission.id,
artifactHashes: asArray(submission.artifactHashes).slice().sort()
})
}));

return {
lockedCount: lockedSubmissions.length,
lockedSubmissions,
evidenceDigest: digest({
teamId: team.teamId,
amendmentId: amendment.id,
lockedSubmissions
})
};
}

function evaluateTeamConsent({ team, amendment, submissions }) {
const consentReceived = asArray(team.consentedAmendmentIds).includes(amendment.id);
const evidenceLock = lockSubmissionEvidence(team, submissions, amendment);
const consentRequired = amendment.requiresReconsent;
const protectedWithdrawal =
consentRequired &&
!consentReceived &&
evidenceLock.lockedCount > 0;

return {
teamId: team.teamId,
amendmentId: amendment.id,
consentRequired,
consentReceived,
protectedWithdrawal,
payoutHold: consentRequired && !consentReceived,
evidenceLock,
status: consentRequired
? consentReceived
? "accepted_changed_terms"
: protectedWithdrawal
? "protected_withdrawal_available"
: "awaiting_consent"
: "no_reconsent_required"
};
}

function buildAmendmentLedger({ challenge, amendments, teams, submissions }) {
if (!challenge || !challenge.id) {
throw new Error("challenge.id is required");
}

const classifiedAmendments = asArray(amendments).map((amendment) =>
classifyAmendment(amendment, challenge)
);

const teamStatuses = [];
for (const amendment of classifiedAmendments) {
for (const team of asArray(teams)) {
teamStatuses.push(evaluateTeamConsent({
team,
amendment,
submissions: asArray(submissions)
}));
}
}

const blockedAwardTeamIds = Array.from(new Set(
teamStatuses
.filter((status) => status.payoutHold)
.map((status) => status.teamId)
)).sort();

const protectedWithdrawalTeamIds = Array.from(new Set(
teamStatuses
.filter((status) => status.protectedWithdrawal)
.map((status) => status.teamId)
)).sort();

const safeToContinueTeamIds = asArray(teams)
.map((team) => team.teamId)
.filter((teamId) => !blockedAwardTeamIds.includes(teamId))
.sort();

return {
challengeId: challenge.id,
challengeTitle: challenge.title,
classifiedAmendments,
teamStatuses,
arbitrationPacket: {
challengeId: challenge.id,
activeMaterialAmendmentIds: classifiedAmendments
.filter((amendment) => amendment.requiresReconsent)
.map((amendment) => amendment.id),
blockedAwardTeamIds,
protectedWithdrawalTeamIds,
safeToContinueTeamIds,
packetDigest: digest({
challenge,
classifiedAmendments,
teamStatuses
})
}
};
}

module.exports = {
buildAmendmentLedger,
classifyAmendment,
digest,
evaluateTeamConsent
};
18 changes: 18 additions & 0 deletions challenge-amendment-consent-ledger/requirements-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Requirements Map

| Issue requirement | Implementation |
| --- | --- |
| Challenge posting portal with deliverables and rubrics | Amendment classifier reads changed deliverables, criteria, deadlines, payout schedules, private-data requirements, and IP terms. |
| Submission engine with version control and audit logs | `lockSubmissionEvidence` preserves pre-amendment submission manifests and artifact hashes. |
| Multi-phase challenges and milestone changes | Deadline, payout schedule, and criteria changes are scored as material amendments. |
| Arbitration and reward distribution | `arbitrationPacket` lists payout holds, safe-to-continue teams, protected withdrawals, and material amendment IDs. |
| IP management options | IP term changes require explicit solver re-consent. |
| Trust for sponsors and solvers | Deterministic digests make amendment decisions and locked evidence auditable. |

## Reviewer Checklist

- Run `node challenge-amendment-consent-ledger/test.js`.
- Run `node challenge-amendment-consent-ledger/demo.js`.
- Confirm material amendments require re-consent.
- Confirm pre-change submissions are locked for protected withdrawal.
- Confirm unconsented material changes hold awards until resolved.
Loading