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
42 changes: 42 additions & 0 deletions challenge-ip-redaction-gate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Challenge IP Redaction Gate

Self-contained milestone for SCIBASE.AI issue #18, Scientific Bounty System.

This module protects solver intellectual property during scientific bounty
evaluation. It gives sponsors and reviewers useful evidence packets while
withholding unpaid proprietary content until the challenge has funded and
settled the relevant award.

## What It Covers

- Sponsor preview packets that redact unpaid solver IP.
- Reviewer packets with NDA and role-aware disclosure controls.
- Private challenge and NDA readiness checks before sponsor access.
- Settlement-gated IP transfer after funded payout.
- Deterministic audit digests for arbitration-ready evidence.
- Synthetic sample data only; no network, credentials, or external services.

## Files

- `index.js` - disclosure policy engine.
- `demo.js` - terminal demo for sponsor/reviewer/settlement flows.
- `test.js` - dependency-free regression tests.
- `demo.mp4` - short demo artifact for the bounty review flow.

## Run

```sh
node challenge-ip-redaction-gate/test.js
node challenge-ip-redaction-gate/demo.js
```

## Requirement Map

| Issue #18 Requirement | Implementation |
| --- | --- |
| Challenge posting portal with public/private support | `validateChallengeDisclosure()` blocks private or sensitive challenge previews unless NDA and IP policies are configured. |
| Secure submission engine | `buildDisclosurePacket()` turns submission artifacts into role-specific evidence packets without exposing unpaid protected content. |
| Anonymous or named participation | Submission `team.displayMode` and audit metadata keep the solver identity separate from artifact disclosure decisions. |
| Arbitration and reward distribution | Deterministic `auditDigest` values and `withheldArtifacts` create reviewer/arbitrator evidence without leaking raw IP. |
| IP management options | `settlement.status === "funded_settled"` is required before sponsor packets disclose protected content or mark `ipTransferReady`. |
| Feedback loop between submitters and sponsors | Packets include `requiredActions` explaining the exact gating reason and remediation path. |
76 changes: 76 additions & 0 deletions challenge-ip-redaction-gate/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const { buildDisclosurePacket } = require("./index")

const challenge = {
id: "bio-marker-forecast-2026",
visibility: "private",
ndaRequired: true,
ipPolicy: "transfer_on_payout",
evaluationMode: "reviewer_only",
sensitiveDomains: ["single-cell-rna", "clinical"],
}

const submission = {
id: "sub-lab-17",
team: {
id: "solver-team-aurora",
displayMode: "anonymous_until_shortlist",
},
artifacts: [
{
id: "whitepaper",
title: "Biomarker Method Summary",
kind: "document",
confidential: true,
ipProtected: false,
content: "Feature-selection method, validation cohort, and benchmark summary.",
safeSummary: "High-level biomarker validation summary is available to NDA reviewers.",
},
{
id: "model-weights",
title: "Trained Model Weights",
kind: "model",
confidential: true,
ipProtected: true,
content: "solver-owned weight matrix and proprietary feature ranking formula",
safeSummary: "Model weights are committed by hash and withheld until payout settlement.",
},
{
id: "clinical-table",
title: "Clinical Feature Table",
kind: "dataset",
confidential: true,
containsHumanSubjectsData: true,
ipProtected: false,
content: "synthetic patient-level feature rows",
safeSummary: "Human-subjects table is held pending ethics clearance.",
},
],
}

const sponsorBeforePayout = buildDisclosurePacket({
challenge,
submission,
actor: { role: "sponsor", id: "sponsor-pharma", ndaSigned: true },
settlement: { status: "under_review", ethicsCleared: false },
})

const reviewerPacket = buildDisclosurePacket({
challenge,
submission,
actor: { role: "reviewer", id: "reviewer-2", ndaSigned: true, clearance: "full" },
settlement: { status: "under_review", ethicsCleared: true },
})

const sponsorAfterSettlement = buildDisclosurePacket({
challenge,
submission,
actor: { role: "sponsor", id: "sponsor-pharma", ndaSigned: true },
settlement: { status: "funded_settled", ethicsCleared: true },
})

console.log("Sponsor before payout")
console.log(JSON.stringify(sponsorBeforePayout, null, 2))
console.log("\nReviewer packet")
console.log(JSON.stringify(reviewerPacket, null, 2))
console.log("\nSponsor after funded settlement")
console.log(JSON.stringify(sponsorAfterSettlement, null, 2))
Binary file added challenge-ip-redaction-gate/demo.mp4
Binary file not shown.
13 changes: 13 additions & 0 deletions challenge-ip-redaction-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.
180 changes: 180 additions & 0 deletions challenge-ip-redaction-gate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
const crypto = require("node:crypto")

const REDACTION_NOTICE =
"[REDACTED: solver IP withheld until funded settlement or authorized reviewer access]"

function stableJson(value) {
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`
if (value && typeof value === "object") {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
.join(",")}}`
}
return JSON.stringify(value)
}

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

function validateChallengeDisclosure(challenge) {
const requiredActions = []

if (!challenge.id) requiredActions.push("Add a stable challenge id.")
if (!challenge.ipPolicy) requiredActions.push("Define an IP policy.")
if (!challenge.evaluationMode) requiredActions.push("Define an evaluation mode.")

if (challenge.visibility === "private" && !challenge.ndaRequired) {
requiredActions.push("Private challenges must require an NDA.")
}

if (
challenge.sensitiveDomains?.length > 0 &&
challenge.evaluationMode === "open_preview" &&
!challenge.ndaRequired
) {
requiredActions.push(
"Sensitive-domain challenges need NDA-gated previews or reviewer-only packets.",
)
}

return {
ready: requiredActions.length === 0,
requiredActions,
}
}

function actorCanViewProtectedIp(actor, challenge, settlement) {
if (actor.role === "sponsor") return settlement?.status === "funded_settled"
if (actor.role === "reviewer") return Boolean(actor.ndaSigned && actor.clearance === "full")
if (actor.role === "arbitrator") return Boolean(actor.ndaSigned)
return false
}

function actorCanViewConfidential(actor, challenge) {
if (actor.role === "sponsor") return !challenge.ndaRequired || actor.ndaSigned
if (actor.role === "reviewer" || actor.role === "arbitrator") return actor.ndaSigned
return false
}

function classifyArtifact(artifact, challenge, actor, settlement) {
if (artifact.ipProtected && !actorCanViewProtectedIp(actor, challenge, settlement)) {
return {
classification: "protected_ip_redacted",
discloseRaw: false,
reason: "Protected solver IP is withheld until funded settlement or authorized reviewer access.",
}
}

if (artifact.confidential && !actorCanViewConfidential(actor, challenge)) {
return {
classification: "confidential_blocked",
discloseRaw: false,
reason: "Confidential artifact requires NDA-gated access.",
}
}

if (artifact.containsHumanSubjectsData && actor.role === "sponsor" && !settlement?.ethicsCleared) {
return {
classification: "ethics_redacted",
discloseRaw: false,
reason: "Human-subjects data requires ethics clearance before sponsor disclosure.",
}
}

return {
classification: "disclosed",
discloseRaw: true,
reason: "Actor is authorized for this artifact.",
}
}

function redactArtifact(artifact, challenge, actor, settlement) {
const decision = classifyArtifact(artifact, challenge, actor, settlement)
const contentHash = digest({
id: artifact.id,
title: artifact.title,
content: artifact.content,
})

if (decision.discloseRaw) {
return {
id: artifact.id,
title: artifact.title,
kind: artifact.kind,
classification: decision.classification,
content: artifact.content,
contentHash,
redacted: false,
}
}

return {
id: artifact.id,
title: artifact.title,
kind: artifact.kind,
classification: decision.classification,
content: artifact.safeSummary || REDACTION_NOTICE,
contentHash,
redacted: true,
redactionReason: decision.reason,
}
}

function buildDisclosurePacket({ challenge, submission, actor, settlement = {} }) {
const challengeReadiness = validateChallengeDisclosure(challenge)
const artifacts = submission.artifacts.map((artifact) =>
redactArtifact(artifact, challenge, actor, settlement),
)
const withheldArtifacts = artifacts.filter((artifact) => artifact.redacted)
const requiredActions = [...challengeReadiness.requiredActions]

for (const artifact of withheldArtifacts) {
requiredActions.push(`${artifact.id}: ${artifact.redactionReason}`)
}

const ipTransferReady =
actor.role === "sponsor" &&
settlement.status === "funded_settled" &&
challenge.ipPolicy === "transfer_on_payout" &&
withheldArtifacts.length === 0

const packet = {
challengeId: challenge.id,
submissionId: submission.id,
actor: {
role: actor.role,
id: actor.id,
},
team: {
id: submission.team.id,
displayMode: submission.team.displayMode,
},
disclosureStatus: requiredActions.length === 0 ? "ready" : "gated",
ipTransferReady,
artifacts,
withheldArtifacts: withheldArtifacts.map((artifact) => ({
id: artifact.id,
title: artifact.title,
classification: artifact.classification,
contentHash: artifact.contentHash,
reason: artifact.redactionReason,
})),
requiredActions,
}

return {
...packet,
auditDigest: digest(packet),
}
}

module.exports = {
REDACTION_NOTICE,
buildDisclosurePacket,
classifyArtifact,
digest,
stableJson,
validateChallengeDisclosure,
}
Loading