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
43 changes: 43 additions & 0 deletions dataset-curation-reuse-credit-ledger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Dataset Curation Reuse Credit Ledger

This is a focused Community & User Reputation System slice for SCIBASE issue #15. It awards or quarantines reputation credit for downstream reuse of curated datasets and code, using citation evidence, license compatibility, self-reuse controls, duplicate evidence checks, and deterministic audit digests.

## Scope

- Allocates curator, validator, and maintainer credit from validated downstream reuse.
- Requires citation evidence before awarding public reputation.
- Checks license compatibility between source asset and reuse event.
- Quarantines self-reuse until independent evidence exists.
- Flags duplicate evidence hashes.
- Emits profile-ready reputation deltas and reuse badges.
- Emits stable `dcrcl_` audit digests for each reuse packet.

It intentionally does not duplicate broad reputation ledgers, review-quality safeguards, abuse/appeals detectors, peer-review calibration, reviewer COI assignment, contributor credit attestation, transparency receipts, correction-impact ledgers, or mentorship impact ladders.

## Run

```powershell
node dataset-curation-reuse-credit-ledger/test.js
node dataset-curation-reuse-credit-ledger/demo.js
```

The demo writes:

- `dataset-curation-reuse-credit-ledger/demo-output/reuse-credit-ledger.json`
- `dataset-curation-reuse-credit-ledger/demo-output/demo.svg`

This PR also includes the required short MP4 demo artifact:

- `dataset-curation-reuse-credit-ledger/demo-output/demo.mp4`

## API

```js
const {
auditReuseCredits,
buildReputationSummary,
createReuseDigest,
} = require("./dataset-curation-reuse-credit-ledger");

const audit = auditReuseCredits({ assets, reuseEvents, generatedAt });
```
27 changes: 27 additions & 0 deletions dataset-curation-reuse-credit-ledger/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Acceptance Notes

## What This Adds

- Dependency-free Node.js module under `dataset-curation-reuse-credit-ledger/`.
- Deterministic reuse-credit audit packets for curated datasets and code.
- Tests for valid credit allocation, unsafe evidence quarantine, profile-ready reputation summaries, and stable reuse digests.
- Demo JSON, SVG, and MP4 artifacts for bounty review.

## Verification

Use these commands from the repository root:

```powershell
node dataset-curation-reuse-credit-ledger/test.js
node dataset-curation-reuse-credit-ledger/demo.js
node --check dataset-curation-reuse-credit-ledger/index.js
node --check dataset-curation-reuse-credit-ledger/test.js
node --check dataset-curation-reuse-credit-ledger/demo.js
node --check dataset-curation-reuse-credit-ledger/sample-data.js
ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 dataset-curation-reuse-credit-ledger/demo-output/demo.mp4
git diff --check
```

## AI Assistance Disclosure

This contribution was prepared with AI assistance from OpenAI Codex and reviewed through local deterministic tests and artifact checks before submission.
Binary file not shown.
57 changes: 57 additions & 0 deletions dataset-curation-reuse-credit-ledger/demo-output/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
"generatedAt": "2026-05-20T13:30:00.000Z",
"reusePackets": [
{
"eventId": "reuse-1",
"assetId": "dataset-cell-42",
"assetTitle": "Single-cell assay benchmark",
"assetType": "dataset",
"generatedAt": "2026-05-20T13:30:00.000Z",
"decision": "award_credit",
"flags": [],
"creditAllocations": [
{
"userId": "u-alina",
"assetId": "dataset-cell-42",
"points": 21,
"reason": "curator reuse credit"
},
{
"userId": "u-minh",
"assetId": "dataset-cell-42",
"points": 9,
"reason": "validator reuse credit"
}
],
"actions": [
"Publish reuse credit to curator profiles"
],
"reuseDigest": "dcrcl_c48af1209078c13469ce3a4d"
},
{
"eventId": "reuse-2",
"assetId": "dataset-cell-42",
"assetTitle": "Single-cell assay benchmark",
"assetType": "dataset",
"generatedAt": "2026-05-20T13:30:00.000Z",
"decision": "quarantine_credit",
"flags": [
"MISSING_CITATION",
"LICENSE_MISMATCH",
"SELF_REUSE",
"DUPLICATE_EVIDENCE"
],
"creditAllocations": [],
"actions": [
"Add citation evidence for reuse-2",
"Resolve CC0 vs CC-BY-4.0 license mismatch",
"Require independent reuse evidence before awarding curator reputation",
"Deduplicate evidence hash sha256:evidence-1"
],
"reuseDigest": "dcrcl_1098b9741d89e72ac0581b45"
},
{
"eventId": "reuse-3",
"assetId": "code-qc-7",
"assetTitle": "QC pipeline",
"assetType": "code",
"generatedAt": "2026-05-20T13:30:00.000Z",
"decision": "award_credit",
"flags": [],
"creditAllocations": [
{
"userId": "u-minh",
"assetId": "code-qc-7",
"points": 15,
"reason": "maintainer reuse credit"
}
],
"actions": [
"Publish reuse credit to curator profiles"
],
"reuseDigest": "dcrcl_bca70185e357d2e0deffdc38"
}
],
"reputationSummary": {
"counts": {
"reuseEvents": 3,
"awarded": 2,
"quarantined": 1,
"duplicateEvidence": 1,
"licenseIssues": 1
},
"reputationDeltas": [
{
"userId": "u-alina",
"points": 21,
"badges": [
"dataset_reuse_curator"
]
},
{
"userId": "u-minh",
"points": 24,
"badges": [
"code_reuse_maintainer",
"dataset_reuse_validator"
]
}
]
}
}
83 changes: 83 additions & 0 deletions dataset-curation-reuse-credit-ledger/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const fs = require("fs");
const path = require("path");

const { auditReuseCredits } = require("./index");
const { assets, reuseEvents } = require("./sample-data");

const generatedAt = "2026-05-20T13:30:00.000Z";
const outputDir = path.join(__dirname, "demo-output");

fs.mkdirSync(outputDir, { recursive: true });

const audit = auditReuseCredits({ assets, reuseEvents, generatedAt });
fs.writeFileSync(path.join(outputDir, "reuse-credit-ledger.json"), `${JSON.stringify(audit, null, 2)}\n`);
fs.writeFileSync(path.join(outputDir, "demo.svg"), buildSvg(audit));

console.log("Dataset curation reuse credit ledger demo");
console.log(`Reuse events: ${audit.reputationSummary.counts.reuseEvents}`);
console.log(`Awarded: ${audit.reputationSummary.counts.awarded}`);
console.log(`Quarantined: ${audit.reputationSummary.counts.quarantined}`);
console.log(`License issues: ${audit.reputationSummary.counts.licenseIssues}`);
console.log(`Wrote ${path.join(outputDir, "reuse-credit-ledger.json")}`);
console.log(`Wrote ${path.join(outputDir, "demo.svg")}`);

function buildSvg(audit) {
const rows = audit.reusePackets.map((packet, index) => {
const y = 196 + index * 82;
const color = packet.decision === "award_credit" ? "#1f8a5b" : "#b42318";
const flags = packet.flags.length === 0 ? "Credit awarded" : packet.flags.join(" | ");
return `<g transform="translate(64 ${y})">
<rect width="832" height="60" rx="8" fill="#ffffff" stroke="#cfd8dc"/>
<circle cx="26" cy="30" r="10" fill="${color}"/>
<text x="48" y="25" class="team">${escapeXml(packet.eventId)} - ${escapeXml(packet.assetTitle)}</text>
<text x="48" y="45" class="meta">${escapeXml(formatDecision(packet.decision))}</text>
<text x="330" y="25" class="small">${escapeXml(flags)}</text>
<text x="330" y="45" class="small">${escapeXml(packet.reuseDigest)}</text>
</g>`;
}).join("");

const deltas = audit.reputationSummary.reputationDeltas
.map((delta) => `${delta.userId} +${delta.points}`)
.join(" ");

return `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="520" viewBox="0 0 960 520" role="img" aria-label="Dataset curation reuse credit ledger demo">
<style>
.title { font: 700 28px Arial, sans-serif; fill: #102027; }
.subtitle { font: 400 15px Arial, sans-serif; fill: #455a64; }
.metric { font: 700 24px Arial, sans-serif; }
.label { font: 400 13px Arial, sans-serif; fill: #546e7a; }
.team { font: 700 16px Arial, sans-serif; fill: #102027; }
.meta { font: 400 13px Arial, sans-serif; fill: #455a64; }
.small { font: 400 11px Arial, sans-serif; fill: #607d8b; }
</style>
<rect width="960" height="520" fill="#f4f7f9"/>
<text x="64" y="58" class="title">Dataset Curation Reuse Credit Ledger</text>
<text x="64" y="84" class="subtitle">Community reputation from downstream dataset and code reuse</text>
${metricCard(64, 112, "Reuse", audit.reputationSummary.counts.reuseEvents, "#0b5fff")}
${metricCard(252, 112, "Awarded", audit.reputationSummary.counts.awarded, "#1f8a5b")}
${metricCard(440, 112, "Quarantine", audit.reputationSummary.counts.quarantined, "#b42318")}
${metricCard(628, 112, "License", audit.reputationSummary.counts.licenseIssues, "#ad6f00")}
${rows}
<text x="64" y="478" class="subtitle">${escapeXml(deltas)}</text>
</svg>`;
}

function metricCard(x, y, label, value, color) {
return `<g transform="translate(${x} ${y})">
<rect width="150" height="58" rx="8" fill="#ffffff" stroke="#cfd8dc"/>
<text x="18" y="25" class="metric" fill="${color}">${value}</text>
<text x="18" y="45" class="label">${escapeXml(label)}</text>
</g>`;
}

function formatDecision(decision) {
return decision.split("_").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
}

function escapeXml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
Loading