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 reputation-correction-impact-ledger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Reputation Correction Impact Ledger

This submission targets [SCIBASE issue #15](https://github.com/SCIBASE-AI/SCIBASE.AI/issues/15) with a focused Community & User Reputation System module.

It handles corrections, retractions, and appeals after reputation receipts already affected a profile. The original receipt remains auditable, while profile score, domain scores, and leaderboard eligibility use corrected evidence only.

## What It Adds

- Immutable receipt digests for reviews, credits, badges, endorsements, and bounty completions.
- Correction events for retractions, amended points, appeal holds, and restores.
- Transparent before/after score deltas for profile review.
- Domain score recomputation so corrected evidence is not double-counted.
- Leaderboard eligibility gating while correction or appeal risk is open.

## Demo

```powershell
node reputation-correction-impact-ledger/test.js
node reputation-correction-impact-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 reputation-correction-impact-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 #15, not a generic AI-generated content drop. The slice targets a specific reputation-system failure mode: profile scores become misleading when peer reviews, contribution credits, or reproducibility badges are corrected after they have already been counted.

## What Changed

- Added immutable receipt digests for profile reputation evidence.
- Added correction events for retractions, amended points, appeal holds, and restores.
- Added transparent before/after score deltas and per-domain recomputation.
- Added leaderboard eligibility gating while unresolved correction risk exists.
- 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 reputation-correction-impact-ledger/test.js
node reputation-correction-impact-ledger/demo.js
```

Expected result: the test prints `reputation-correction-impact-ledger tests passed`, and the demo prints the original-to-corrected profile score, revoked receipts, amended receipts, appeal windows, leaderboard eligibility, and correction packet digest.

## Integration Notes

The module is dependency-free and uses plain profile receipt and correction objects. The next integration step is wiring receipts to SCIBASE review, contributor-credit, badge, and profile timeline events.
65 changes: 65 additions & 0 deletions reputation-correction-impact-ledger/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use strict";

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

const ledger = buildCorrectionLedger({
profileId: "researcher-ada",
receipts: [
{
id: "review-7",
kind: "peer_review",
projectId: "neuro-biomarker-atlas",
domain: "review",
points: 28
},
{
id: "credit-3",
kind: "contribution_credit",
projectId: "neuro-biomarker-atlas",
domain: "credit",
points: 18
},
{
id: "badge-2",
kind: "reproducibility_badge",
projectId: "climate-model-rerun",
domain: "reproducibility",
points: 26
}
],
corrections: [
{
id: "corr-1",
targetReceiptId: "review-7",
action: "retract",
reason: "reviewer conflict disclosed after publication",
decidedAt: "2026-05-10T09:00:00Z"
},
{
id: "corr-2",
targetReceiptId: "credit-3",
action: "amend_points",
correctedPoints: 8,
reason: "role changed from primary curation to verification support",
decidedAt: "2026-05-11T12:00:00Z"
},
{
id: "corr-3",
targetReceiptId: "badge-2",
action: "appeal_hold",
reason: "rerun package is under independent review",
appealUntil: "2026-05-22T23:59:59Z",
decidedAt: "2026-05-12T12:00:00Z"
}
]
});

console.log(JSON.stringify({
profileId: ledger.profileId,
score: `${ledger.originalScore} -> ${ledger.correctedScore}`,
revokedReceiptIds: ledger.revokedReceiptIds,
amendedReceiptIds: ledger.amendedReceiptIds,
appealWindows: ledger.appealWindows,
leaderboardEligible: ledger.leaderboardEligible,
correctionPacketDigest: ledger.correctionPacketDigest
}, null, 2));
Binary file added reputation-correction-impact-ledger/demo.mp4
Binary file not shown.
23 changes: 23 additions & 0 deletions reputation-correction-impact-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.
172 changes: 172 additions & 0 deletions reputation-correction-impact-ledger/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"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((accumulator, key) => {
accumulator[key] = stableJson(value[key]);
return accumulator;
}, {});
}
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 receiptPoints(receipt) {
if (typeof receipt.points === "number") return receipt.points;
const defaults = {
peer_review: 18,
contribution_credit: 12,
reproducibility_badge: 24,
endorsement: 8,
bounty_completion: 30
};
return defaults[receipt.kind] || 0;
}

function buildCorrectionIndex(corrections) {
const index = new Map();
for (const correction of asArray(corrections).slice().sort((a, b) =>
String(a.decidedAt || "").localeCompare(String(b.decidedAt || ""))
)) {
const list = index.get(correction.targetReceiptId) || [];
list.push(correction);
index.set(correction.targetReceiptId, list);
}
return index;
}

function applyReceiptCorrections(receipt, corrections) {
let correctedPoints = receiptPoints(receipt);
let status = "active";
const appliedCorrections = [];
const appealWindows = [];

for (const correction of asArray(corrections)) {
appliedCorrections.push({
id: correction.id,
action: correction.action,
reason: correction.reason || "not provided",
decidedAt: correction.decidedAt || null
});

if (correction.action === "retract") {
correctedPoints = 0;
status = "retracted";
}

if (correction.action === "amend_points") {
correctedPoints = correction.correctedPoints;
status = "amended";
}

if (correction.action === "appeal_hold") {
status = status === "active" ? "under_appeal" : status;
appealWindows.push({
correctionId: correction.id,
appealUntil: correction.appealUntil,
reason: correction.reason || "appeal pending"
});
}

if (correction.action === "restore") {
correctedPoints = correction.restoredPoints ?? receiptPoints(receipt);
status = "restored";
}
}

return {
receiptId: receipt.id,
kind: receipt.kind,
projectId: receipt.projectId || null,
domain: receipt.domain || "general",
originalPoints: receiptPoints(receipt),
correctedPoints,
status,
appliedCorrections,
appealWindows,
originalDigest: digest(receipt),
correctedDigest: digest({
receiptId: receipt.id,
correctedPoints,
status,
appliedCorrections
})
};
}

function summarizeDomainScores(adjustedReceipts) {
const scores = {};
for (const receipt of adjustedReceipts) {
scores[receipt.domain] = (scores[receipt.domain] || 0) + receipt.correctedPoints;
}
return Object.keys(scores).sort().map((domain) => ({
domain,
score: scores[domain]
}));
}

function buildCorrectionLedger({ profileId, receipts, corrections }) {
if (!profileId) {
throw new Error("profileId is required");
}

const correctionIndex = buildCorrectionIndex(corrections);
const adjustedReceipts = asArray(receipts).map((receipt) =>
applyReceiptCorrections(receipt, correctionIndex.get(receipt.id))
);

const originalScore = adjustedReceipts.reduce((sum, receipt) => sum + receipt.originalPoints, 0);
const correctedScore = adjustedReceipts.reduce((sum, receipt) => sum + receipt.correctedPoints, 0);
const revokedReceiptIds = adjustedReceipts
.filter((receipt) => receipt.status === "retracted")
.map((receipt) => receipt.receiptId)
.sort();
const amendedReceiptIds = adjustedReceipts
.filter((receipt) => receipt.status === "amended")
.map((receipt) => receipt.receiptId)
.sort();
const appealWindows = adjustedReceipts.flatMap((receipt) =>
receipt.appealWindows.map((window) => ({
receiptId: receipt.receiptId,
...window
}))
);

return {
profileId,
originalScore,
correctedScore,
scoreDelta: correctedScore - originalScore,
revokedReceiptIds,
amendedReceiptIds,
appealWindows,
leaderboardEligible: appealWindows.length === 0 && revokedReceiptIds.length === 0,
domainScores: summarizeDomainScores(adjustedReceipts),
adjustedReceipts,
correctionPacketDigest: digest({
profileId,
adjustedReceipts,
correctedScore,
appealWindows
})
};
}

module.exports = {
applyReceiptCorrections,
buildCorrectionLedger,
digest
};
18 changes: 18 additions & 0 deletions reputation-correction-impact-ledger/requirements-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Requirements Map

| Issue requirement | Implementation |
| --- | --- |
| Peer reviews and comments influence reputation | Peer-review receipts carry points and immutable evidence digests. |
| Contributor credits are timestamped and credited | Contribution receipts can be amended without deleting the original record. |
| Transparent reputation metrics | `originalScore`, `correctedScore`, `scoreDelta`, and domain scores explain exactly what changed. |
| Reproducibility badge and peer validation support | Badge receipts can enter appeal hold while independent review completes. |
| Leaderboards and badge system | `leaderboardEligible` blocks profiles with unresolved retractions or appeal windows. |
| Profile history and project timelines | Original receipt digests are preserved in each adjusted receipt. |

## Reviewer Checklist

- Run `node reputation-correction-impact-ledger/test.js`.
- Run `node reputation-correction-impact-ledger/demo.js`.
- Confirm retracted review receipts score as zero but keep their original digest.
- Confirm amended contribution credits replace points instead of double-counting.
- Confirm appeal holds block leaderboard eligibility.
Loading