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
34 changes: 34 additions & 0 deletions project-contribution-credit-gate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Project Contribution Credit Gate

Self-contained review slice for issue #11, User & Project Management.

This module validates contribution credit packets before a scientific project is published. It focuses on authorship consent, CRediT role coverage, and artifact contributor omissions rather than broad RBAC, invitation expiry, service-account governance, break-glass access, deletion/erasure, or funding attribution.

## What it checks

- Listed contributors have explicitly consented to the credit statement.
- Credited contributors have at least one CRediT taxonomy role.
- Contributors who touched manuscript, code, data, or result artifacts are credited or routed for review.
- Deterministic reviewer actions are emitted for publication blockers.

## Files

- `index.js` - contribution credit evaluator and report builder.
- `sample-data.js` - synthetic project/contributor scenarios.
- `test.js` - Node.js built-in test suite.
- `demo.js` - generates reviewer artifacts in `reports/`.
- `reports/credit-packet.json` - generated machine-readable decisions.
- `reports/credit-report.md` - generated reviewer packet.
- `reports/summary.svg` - generated visual summary.
- `reports/demo.mp4` - short demo video artifact for bounty review.

## Run

```bash
node project-contribution-credit-gate/test.js
node project-contribution-credit-gate/demo.js
```

## Safety

The sample data is synthetic. The module performs no network calls, touches no live identity providers, and contains no secrets, tokens, private dashboard data, payout information, ORCID credentials, SAML/OAuth data, or private project records.
49 changes: 49 additions & 0 deletions project-contribution-credit-gate/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const fs = require('node:fs');
const path = require('node:path');

const {evaluateContributionCredit, buildCreditReport} = require('./index');
const {scenarios} = require('./sample-data');

const reportsDir = path.join(__dirname, 'reports');
fs.mkdirSync(reportsDir, {recursive: true});

const evaluations = scenarios.map((scenario) => ({
scenario: scenario.name,
...evaluateContributionCredit(scenario),
}));

const approved = evaluations.filter((item) => item.decision === 'approved').length;
const remediation = evaluations.filter((item) => item.decision === 'needs-remediation').length;
const blocked = evaluations.filter((item) => item.decision === 'blocked').length;

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540">
<rect width="960" height="540" fill="#111827"/>
<text x="48" y="72" fill="#f9fafb" font-family="Arial, sans-serif" font-size="34" font-weight="700">Project Contribution Credit Gate</text>
<text x="48" y="112" fill="#d1d5db" font-family="Arial, sans-serif" font-size="18">Synthetic authorship and CRediT statement review packet</text>
<g transform="translate(48 160)">
<rect width="250" height="160" rx="14" fill="#065f46"/>
<text x="24" y="52" fill="#d1fae5" font-family="Arial, sans-serif" font-size="20" font-weight="700">Approved</text>
<text x="24" y="112" fill="#ecfdf5" font-family="Arial, sans-serif" font-size="58" font-weight="700">${approved}</text>
</g>
<g transform="translate(354 160)">
<rect width="250" height="160" rx="14" fill="#92400e"/>
<text x="24" y="52" fill="#fef3c7" font-family="Arial, sans-serif" font-size="20" font-weight="700">Needs Remediation</text>
<text x="24" y="112" fill="#fffbeb" font-family="Arial, sans-serif" font-size="58" font-weight="700">${remediation}</text>
</g>
<g transform="translate(660 160)">
<rect width="250" height="160" rx="14" fill="#991b1b"/>
<text x="24" y="52" fill="#fee2e2" font-family="Arial, sans-serif" font-size="20" font-weight="700">Blocked</text>
<text x="24" y="112" fill="#fef2f2" font-family="Arial, sans-serif" font-size="58" font-weight="700">${blocked}</text>
</g>
<text x="48" y="390" fill="#e5e7eb" font-family="Arial, sans-serif" font-size="22">Checks: consent, CRediT roles, artifact contributors, credit omissions</text>
<text x="48" y="430" fill="#9ca3af" font-family="Arial, sans-serif" font-size="18">Outputs: credit-packet.json, credit-report.md, summary.svg, demo video artifact</text>
<text x="48" y="478" fill="#6b7280" font-family="Arial, sans-serif" font-size="16">Synthetic data only. No live ORCID, SAML, OAuth, profile, or private project calls.</text>
</svg>
`;

fs.writeFileSync(path.join(reportsDir, 'credit-packet.json'), `${JSON.stringify(evaluations, null, 2)}\n`);
fs.writeFileSync(path.join(reportsDir, 'credit-report.md'), evaluations.map(buildCreditReport).join('\n---\n'));
fs.writeFileSync(path.join(reportsDir, 'summary.svg'), svg);

console.log(`Wrote ${evaluations.length} contribution credit evaluations to ${reportsDir}`);
console.log(`Decision counts: approved=${approved}, remediation=${remediation}, blocked=${blocked}`);
118 changes: 118 additions & 0 deletions project-contribution-credit-gate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
function list(value) {
return Array.isArray(value) ? value : [];
}

function action(type, target, reason) {
return {type, target, reason};
}

function unique(values) {
return [...new Set(values)];
}

function contributorName(contributorsById, contributorId) {
return contributorsById.get(contributorId)?.name || contributorId;
}

function evaluateContributionCredit(input) {
const contributors = list(input.contributors);
const statements = list(input.creditStatements);
const artifacts = list(input.projectArtifacts);
const contributorsById = new Map(contributors.map((contributor) => [contributor.id, contributor]));
const statementsById = new Map(statements.map((statement) => [statement.contributorId, statement]));
const blockers = [];
const requiredActions = [];

const artifactContributorIds = unique(artifacts.flatMap((artifact) => list(artifact.touchedBy)));

for (const statement of statements) {
const name = contributorName(contributorsById, statement.contributorId);
if (!statement.consented) {
blockers.push(`${name} is listed in the credit statement without consent`);
requiredActions.push(action(
'collect_credit_consent',
statement.contributorId,
'publication credit requires explicit contributor consent'
));
}
if (list(statement.creditRoles).length === 0) {
blockers.push(`${name} has no CRediT roles assigned`);
requiredActions.push(action(
'assign_credit_roles',
statement.contributorId,
'credited contributors need at least one CRediT taxonomy role'
));
}
}

for (const contributorId of artifactContributorIds) {
if (!statementsById.has(contributorId)) {
const name = contributorName(contributorsById, contributorId);
blockers.push(`${name} contributed to project artifacts but is missing from credit statement`);
requiredActions.push(action(
'review_omitted_artifact_contributor',
contributorId,
'artifact contributions must be credited or explicitly excluded with review evidence'
));
}
}

const hasConsentBlocker = blockers.some((item) => /without consent/i.test(item));
const consentedCount = statements.filter((statement) => statement.consented).length;
const decision = hasConsentBlocker
? 'blocked'
: blockers.length > 0
? 'needs-remediation'
: 'approved';

return {
projectId: input.projectId,
action: input.action,
generatedAt: input.generatedAt,
decision,
blockers,
requiredActions,
coverage: {
artifactContributorCount: artifactContributorIds.length,
creditedContributorCount: statements.length,
consentCoverage: statements.length === 0 ? 1 : consentedCount / statements.length,
},
};
}

function percent(value) {
return `${Math.round(value * 100)}%`;
}

function buildCreditReport(result) {
return [
'# Project Contribution Credit Gate Report',
'',
`Project: ${result.projectId}`,
`Action: ${result.action}`,
`Generated: ${result.generatedAt}`,
`Decision: ${result.decision}`,
'',
'## Coverage',
'',
`Artifact contributors: ${result.coverage.artifactContributorCount}`,
`Credited contributors: ${result.coverage.creditedContributorCount}`,
`Consent coverage: ${percent(result.coverage.consentCoverage)}`,
'',
'## Blockers',
'',
...(result.blockers.length ? result.blockers.map((item) => `- ${item}`) : ['- None']),
'',
'## Required Actions',
'',
...(result.requiredActions.length
? result.requiredActions.map((item) => `- ${item.type}: ${item.target} (${item.reason})`)
: ['- None']),
'',
].join('\n');
}

module.exports = {
evaluateContributionCredit,
buildCreditReport,
};
66 changes: 66 additions & 0 deletions project-contribution-credit-gate/reports/credit-packet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
[
{
"scenario": "consent-block",
"projectId": "project-neuro-imaging",
"action": "publish-preprint",
"generatedAt": "2026-05-22T12:30:00Z",
"decision": "blocked",
"blockers": [
"Mika Rao is listed in the credit statement without consent"
],
"requiredActions": [
{
"type": "collect_credit_consent",
"target": "u2",
"reason": "publication credit requires explicit contributor consent"
}
],
"coverage": {
"artifactContributorCount": 2,
"creditedContributorCount": 2,
"consentCoverage": 0.5
}
},
{
"scenario": "omitted-contributor-remediation",
"projectId": "project-climate-model",
"action": "publish-preprint",
"generatedAt": "2026-05-22T12:30:00Z",
"decision": "needs-remediation",
"blockers": [
"Jon Bell has no CRediT roles assigned",
"Ren Ito contributed to project artifacts but is missing from credit statement"
],
"requiredActions": [
{
"type": "assign_credit_roles",
"target": "u2",
"reason": "credited contributors need at least one CRediT taxonomy role"
},
{
"type": "review_omitted_artifact_contributor",
"target": "u3",
"reason": "artifact contributions must be credited or explicitly excluded with review evidence"
}
],
"coverage": {
"artifactContributorCount": 2,
"creditedContributorCount": 2,
"consentCoverage": 1
}
},
{
"scenario": "approved-credit-packet",
"projectId": "project-approved-credit",
"action": "publish-preprint",
"generatedAt": "2026-05-22T12:30:00Z",
"decision": "approved",
"blockers": [],
"requiredActions": [],
"coverage": {
"artifactContributorCount": 2,
"creditedContributorCount": 2,
"consentCoverage": 1
}
}
]
66 changes: 66 additions & 0 deletions project-contribution-credit-gate/reports/credit-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Project Contribution Credit Gate Report

Project: project-neuro-imaging
Action: publish-preprint
Generated: 2026-05-22T12:30:00Z
Decision: blocked

## Coverage

Artifact contributors: 2
Credited contributors: 2
Consent coverage: 50%

## Blockers

- Mika Rao is listed in the credit statement without consent

## Required Actions

- collect_credit_consent: u2 (publication credit requires explicit contributor consent)

---
# Project Contribution Credit Gate Report

Project: project-climate-model
Action: publish-preprint
Generated: 2026-05-22T12:30:00Z
Decision: needs-remediation

## Coverage

Artifact contributors: 2
Credited contributors: 2
Consent coverage: 100%

## Blockers

- Jon Bell has no CRediT roles assigned
- Ren Ito contributed to project artifacts but is missing from credit statement

## Required Actions

- assign_credit_roles: u2 (credited contributors need at least one CRediT taxonomy role)
- review_omitted_artifact_contributor: u3 (artifact contributions must be credited or explicitly excluded with review evidence)

---
# Project Contribution Credit Gate Report

Project: project-approved-credit
Action: publish-preprint
Generated: 2026-05-22T12:30:00Z
Decision: approved

## Coverage

Artifact contributors: 2
Credited contributors: 2
Consent coverage: 100%

## Blockers

- None

## Required Actions

- None
Binary file added project-contribution-credit-gate/reports/demo.mp4
Binary file not shown.
23 changes: 23 additions & 0 deletions project-contribution-credit-gate/reports/summary.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading