diff --git a/credit-role-evidence-guard/README.md b/credit-role-evidence-guard/README.md
new file mode 100644
index 00000000..60adfd59
--- /dev/null
+++ b/credit-role-evidence-guard/README.md
@@ -0,0 +1,30 @@
+# Credit Role Evidence Consistency Guard
+
+This module adds a focused guard for the Community & User Reputation System bounty.
+It validates whether CRediT-style contribution claims have enough artifact evidence
+before those claims are shown on profiles, citation pages, leaderboards, or
+institutional exports.
+
+The guard is intentionally dependency-free and uses synthetic data only. It does
+not call external services, inspect private projects, or contain credentials.
+
+## What It Checks
+
+- Unsupported or misspelled CRediT role names
+- Contributor consent before profile-visible credit is published
+- Missing artifact evidence for a claimed role
+- Evidence type mismatches, such as Software credit with only manuscript evidence
+- Anonymous or blind-review evidence that would leak identity if made public
+- Peer-review credit on a project where the reviewer is also an author
+- Duplicate role/evidence claims that could inflate reputation scores
+- Stale evidence that should be refreshed before reputation deltas are applied
+
+## Usage
+
+```bash
+node credit-role-evidence-guard/test.js
+node credit-role-evidence-guard/demo.js
+```
+
+The demo writes reviewer artifacts to `credit-role-evidence-guard/reports/`.
+
diff --git a/credit-role-evidence-guard/demo.js b/credit-role-evidence-guard/demo.js
new file mode 100644
index 00000000..46681a6e
--- /dev/null
+++ b/credit-role-evidence-guard/demo.js
@@ -0,0 +1,30 @@
+import { mkdir, writeFile } from "node:fs/promises";
+import { buildMarkdownReport, buildSummarySvg, evaluateCreditClaims } from "./index.js";
+import { sampleArtifacts, sampleCreditClaims, sampleProjects } from "./sample-data.js";
+
+const result = evaluateCreditClaims({
+ projects: sampleProjects,
+ artifacts: sampleArtifacts,
+ claims: sampleCreditClaims,
+});
+
+await mkdir(new URL("./reports/", import.meta.url), { recursive: true });
+
+await writeFile(
+ new URL("./reports/reviewer-packet.json", import.meta.url),
+ `${JSON.stringify(result, null, 2)}\n`,
+);
+await writeFile(
+ new URL("./reports/reviewer-report.md", import.meta.url),
+ buildMarkdownReport(result),
+);
+await writeFile(
+ new URL("./reports/summary.svg", import.meta.url),
+ buildSummarySvg(result),
+);
+
+console.log("Generated credit role evidence guard reports:");
+console.log("- credit-role-evidence-guard/reports/reviewer-packet.json");
+console.log("- credit-role-evidence-guard/reports/reviewer-report.md");
+console.log("- credit-role-evidence-guard/reports/summary.svg");
+
diff --git a/credit-role-evidence-guard/index.js b/credit-role-evidence-guard/index.js
new file mode 100644
index 00000000..e5730262
--- /dev/null
+++ b/credit-role-evidence-guard/index.js
@@ -0,0 +1,304 @@
+const ROLE_EVIDENCE_TYPES = {
+ "Conceptualization": ["protocol", "manuscript", "hypothesis"],
+ "Data curation": ["dataset", "data-dictionary", "metadata"],
+ "Formal analysis": ["code", "notebook", "results"],
+ "Funding acquisition": ["grant", "funder-award", "budget"],
+ "Investigation": ["protocol", "dataset", "notebook"],
+ "Methodology": ["protocol", "manuscript", "code"],
+ "Peer review": ["peer-review", "inline-comment", "review-decision"],
+ "Project administration": ["project-plan", "milestone", "review-decision"],
+ "Resources": ["dataset", "material", "instrument"],
+ "Software": ["code", "test", "notebook"],
+ "Supervision": ["protocol", "approval", "project-plan"],
+ "Validation": ["test", "reproducibility-report", "notebook", "results"],
+ "Visualization": ["figure", "plot", "notebook"],
+ "Writing - original draft": ["manuscript", "preprint"],
+ "Writing - review & editing": ["manuscript", "inline-comment", "peer-review"],
+};
+
+const HOLD = "hold";
+const REJECT = "reject";
+const APPROVE = "approve";
+
+function byId(items) {
+ return new Map(items.map((item) => [item.id, item]));
+}
+
+function toTimestamp(value) {
+ const timestamp = Date.parse(value);
+ return Number.isNaN(timestamp) ? null : timestamp;
+}
+
+function duplicateKey(claim) {
+ return [
+ claim.projectId,
+ claim.contributorId,
+ claim.role,
+ [...claim.evidenceIds].sort().join("+"),
+ ].join("|");
+}
+
+function makeFinding(code, severity, message, remediation) {
+ return { code, severity, message, remediation };
+}
+
+export function evaluateCreditClaims({ projects, artifacts, claims }, options = {}) {
+ const projectIndex = byId(projects);
+ const artifactIndex = byId(artifacts);
+ const duplicateCounts = new Map();
+ const freshnessDays = options.freshnessDays ?? 120;
+ const now = toTimestamp(options.now ?? "2026-05-22T12:00:00Z");
+
+ for (const claim of claims) {
+ const key = duplicateKey(claim);
+ duplicateCounts.set(key, (duplicateCounts.get(key) ?? 0) + 1);
+ }
+
+ const decisions = claims.map((claim) =>
+ evaluateSingleClaim({
+ claim,
+ project: projectIndex.get(claim.projectId),
+ artifacts: claim.evidenceIds.map((id) => artifactIndex.get(id)).filter(Boolean),
+ missingEvidenceIds: claim.evidenceIds.filter((id) => !artifactIndex.has(id)),
+ duplicateCount: duplicateCounts.get(duplicateKey(claim)) ?? 0,
+ freshnessDays,
+ now,
+ }),
+ );
+
+ const summary = decisions.reduce(
+ (acc, decision) => {
+ acc[decision.decision] += 1;
+ acc.totalFindings += decision.findings.length;
+ acc.riskScore += decision.riskScore;
+ return acc;
+ },
+ { approve: 0, hold: 0, reject: 0, totalFindings: 0, riskScore: 0 },
+ );
+
+ return {
+ generatedAt: new Date(now).toISOString(),
+ summary,
+ decisions,
+ };
+}
+
+export function evaluateSingleClaim({
+ claim,
+ project,
+ artifacts,
+ missingEvidenceIds,
+ duplicateCount,
+ freshnessDays,
+ now,
+}) {
+ const findings = [];
+ const allowedTypes = ROLE_EVIDENCE_TYPES[claim.role];
+
+ if (!project) {
+ findings.push(
+ makeFinding(
+ "unknown-project",
+ REJECT,
+ `Project ${claim.projectId} does not exist in the repository index.`,
+ "Attach the claim to a known project before credit is published.",
+ ),
+ );
+ }
+
+ if (!allowedTypes) {
+ findings.push(
+ makeFinding(
+ "unsupported-credit-role",
+ REJECT,
+ `Role "${claim.role}" is not in the supported CRediT role map.`,
+ "Normalize the role to a supported taxonomy value.",
+ ),
+ );
+ }
+
+ if (!claim.consentGranted) {
+ findings.push(
+ makeFinding(
+ "missing-profile-consent",
+ HOLD,
+ `${claim.contributorName} has not consented to publish this credit in ${claim.profileVisibility}.`,
+ "Collect contributor consent or keep the credit internal.",
+ ),
+ );
+ }
+
+ if (missingEvidenceIds.length > 0) {
+ findings.push(
+ makeFinding(
+ "missing-evidence",
+ REJECT,
+ `Claim references missing evidence: ${missingEvidenceIds.join(", ")}.`,
+ "Attach existing artifact ids with stable hashes.",
+ ),
+ );
+ }
+
+ if (artifacts.length === 0) {
+ findings.push(
+ makeFinding(
+ "empty-evidence-set",
+ REJECT,
+ "No artifact evidence is attached to the contribution claim.",
+ "Attach at least one artifact matching the claimed role.",
+ ),
+ );
+ }
+
+ if (allowedTypes && artifacts.length > 0) {
+ const matchedTypes = artifacts.filter((artifact) => allowedTypes.includes(artifact.type));
+ if (matchedTypes.length === 0) {
+ findings.push(
+ makeFinding(
+ "role-evidence-mismatch",
+ HOLD,
+ `Role "${claim.role}" expects ${allowedTypes.join(", ")} evidence, but got ${artifacts.map((artifact) => artifact.type).join(", ")}.`,
+ "Attach role-appropriate evidence or change the claimed role.",
+ ),
+ );
+ }
+ }
+
+ if (project?.authors.includes(claim.contributorId) && claim.role === "Peer review") {
+ findings.push(
+ makeFinding(
+ "author-review-credit-conflict",
+ REJECT,
+ "A project author cannot receive peer-review reputation credit for reviewing their own project.",
+ "Route the review to an independent reviewer or record it as author response work.",
+ ),
+ );
+ }
+
+ if (
+ claim.profileVisibility === "public" &&
+ artifacts.some((artifact) => artifact.visibility === "blind-review")
+ ) {
+ findings.push(
+ makeFinding(
+ "blind-review-identity-leak",
+ HOLD,
+ "Public profile credit is backed by blind-review evidence that could expose reviewer identity.",
+ "Publish an anonymized receipt or keep the credit private until the review mode permits disclosure.",
+ ),
+ );
+ }
+
+ if (duplicateCount > 1) {
+ findings.push(
+ makeFinding(
+ "duplicate-credit-claim",
+ HOLD,
+ "Another claim uses the same contributor, role, project, and evidence set.",
+ "Deduplicate before computing profile reputation deltas.",
+ ),
+ );
+ }
+
+ const staleArtifacts = artifacts.filter((artifact) => {
+ const artifactTime = toTimestamp(artifact.createdAt);
+ if (!artifactTime || !now) return false;
+ const ageDays = (now - artifactTime) / (1000 * 60 * 60 * 24);
+ return ageDays > freshnessDays;
+ });
+
+ if (staleArtifacts.length > 0) {
+ findings.push(
+ makeFinding(
+ "stale-evidence",
+ HOLD,
+ `Evidence is older than ${freshnessDays} days: ${staleArtifacts.map((artifact) => artifact.id).join(", ")}.`,
+ "Refresh evidence hashes or add a maintainer attestation before publishing new reputation changes.",
+ ),
+ );
+ }
+
+ const hasReject = findings.some((finding) => finding.severity === REJECT);
+ const hasHold = findings.some((finding) => finding.severity === HOLD);
+ const decision = hasReject ? REJECT : hasHold ? HOLD : APPROVE;
+ const riskScore = findings.reduce((score, finding) => score + (finding.severity === REJECT ? 8 : 3), 0);
+
+ return {
+ claimId: claim.id,
+ contributorId: claim.contributorId,
+ contributorName: claim.contributorName,
+ projectId: claim.projectId,
+ role: claim.role,
+ decision,
+ riskScore,
+ evidenceIds: claim.evidenceIds,
+ findings,
+ };
+}
+
+export function buildMarkdownReport(result) {
+ const lines = [
+ "# Credit Role Evidence Consistency Report",
+ "",
+ `Generated: ${result.generatedAt}`,
+ "",
+ "## Summary",
+ "",
+ `- Approved: ${result.summary.approve}`,
+ `- Held for steward review: ${result.summary.hold}`,
+ `- Rejected before publication: ${result.summary.reject}`,
+ `- Findings: ${result.summary.totalFindings}`,
+ `- Aggregate risk score: ${result.summary.riskScore}`,
+ "",
+ "## Decisions",
+ "",
+ ];
+
+ for (const decision of result.decisions) {
+ lines.push(`### ${decision.claimId}: ${decision.decision.toUpperCase()}`);
+ lines.push("");
+ lines.push(`- Contributor: ${decision.contributorName} (${decision.contributorId})`);
+ lines.push(`- Project: ${decision.projectId}`);
+ lines.push(`- Role: ${decision.role}`);
+ lines.push(`- Risk score: ${decision.riskScore}`);
+ if (decision.findings.length === 0) {
+ lines.push("- Findings: none");
+ } else {
+ for (const finding of decision.findings) {
+ lines.push(`- ${finding.code}: ${finding.message} Remediation: ${finding.remediation}`);
+ }
+ }
+ lines.push("");
+ }
+
+ return `${lines.join("\n")}\n`;
+}
+
+export function buildSummarySvg(result) {
+ const width = 960;
+ const height = 540;
+ const approved = result.summary.approve;
+ const held = result.summary.hold;
+ const rejected = result.summary.reject;
+ const total = Math.max(1, approved + held + rejected);
+ const barWidth = 680;
+ const approveWidth = Math.round((approved / total) * barWidth);
+ const holdWidth = Math.round((held / total) * barWidth);
+ const rejectWidth = barWidth - approveWidth - holdWidth;
+
+ return `
+`;
+}
+
diff --git a/credit-role-evidence-guard/reports/demo.mp4 b/credit-role-evidence-guard/reports/demo.mp4
new file mode 100644
index 00000000..87c438b3
Binary files /dev/null and b/credit-role-evidence-guard/reports/demo.mp4 differ
diff --git a/credit-role-evidence-guard/reports/reviewer-packet.json b/credit-role-evidence-guard/reports/reviewer-packet.json
new file mode 100644
index 00000000..4bd5ad5a
--- /dev/null
+++ b/credit-role-evidence-guard/reports/reviewer-packet.json
@@ -0,0 +1,151 @@
+{
+ "generatedAt": "2026-05-22T12:00:00.000Z",
+ "summary": {
+ "approve": 1,
+ "hold": 4,
+ "reject": 2,
+ "totalFindings": 7,
+ "riskScore": 31
+ },
+ "decisions": [
+ {
+ "claimId": "claim-001",
+ "contributorId": "user-mei",
+ "contributorName": "Mei Lin",
+ "projectId": "proj-neuro-open-001",
+ "role": "Data curation",
+ "decision": "hold",
+ "riskScore": 3,
+ "evidenceIds": [
+ "artifact-dataset-raw-001",
+ "artifact-data-dictionary-001"
+ ],
+ "findings": [
+ {
+ "code": "duplicate-credit-claim",
+ "severity": "hold",
+ "message": "Another claim uses the same contributor, role, project, and evidence set.",
+ "remediation": "Deduplicate before computing profile reputation deltas."
+ }
+ ]
+ },
+ {
+ "claimId": "claim-002",
+ "contributorId": "user-nora",
+ "contributorName": "Nora Hart",
+ "projectId": "proj-climate-model-014",
+ "role": "Software",
+ "decision": "approve",
+ "riskScore": 0,
+ "evidenceIds": [
+ "artifact-analysis-code-001"
+ ],
+ "findings": []
+ },
+ {
+ "claimId": "claim-003",
+ "contributorId": "user-ada",
+ "contributorName": "Ada Pike",
+ "projectId": "proj-neuro-open-001",
+ "role": "Peer review",
+ "decision": "reject",
+ "riskScore": 11,
+ "evidenceIds": [
+ "artifact-review-blind-001"
+ ],
+ "findings": [
+ {
+ "code": "author-review-credit-conflict",
+ "severity": "reject",
+ "message": "A project author cannot receive peer-review reputation credit for reviewing their own project.",
+ "remediation": "Route the review to an independent reviewer or record it as author response work."
+ },
+ {
+ "code": "blind-review-identity-leak",
+ "severity": "hold",
+ "message": "Public profile credit is backed by blind-review evidence that could expose reviewer identity.",
+ "remediation": "Publish an anonymized receipt or keep the credit private until the review mode permits disclosure."
+ }
+ ]
+ },
+ {
+ "claimId": "claim-004",
+ "contributorId": "user-ravi",
+ "contributorName": "Ravi Singh",
+ "projectId": "proj-climate-model-014",
+ "role": "Software",
+ "decision": "hold",
+ "riskScore": 3,
+ "evidenceIds": [
+ "artifact-manuscript-001"
+ ],
+ "findings": [
+ {
+ "code": "role-evidence-mismatch",
+ "severity": "hold",
+ "message": "Role \"Software\" expects code, test, notebook evidence, but got manuscript.",
+ "remediation": "Attach role-appropriate evidence or change the claimed role."
+ }
+ ]
+ },
+ {
+ "claimId": "claim-005",
+ "contributorId": "user-lena",
+ "contributorName": "Lena Rowe",
+ "projectId": "proj-climate-model-014",
+ "role": "Writing - original draft",
+ "decision": "hold",
+ "riskScore": 3,
+ "evidenceIds": [
+ "artifact-manuscript-001"
+ ],
+ "findings": [
+ {
+ "code": "missing-profile-consent",
+ "severity": "hold",
+ "message": "Lena Rowe has not consented to publish this credit in institutional-export.",
+ "remediation": "Collect contributor consent or keep the credit internal."
+ }
+ ]
+ },
+ {
+ "claimId": "claim-006",
+ "contributorId": "user-mei",
+ "contributorName": "Mei Lin",
+ "projectId": "proj-neuro-open-001",
+ "role": "Data curation",
+ "decision": "hold",
+ "riskScore": 3,
+ "evidenceIds": [
+ "artifact-dataset-raw-001",
+ "artifact-data-dictionary-001"
+ ],
+ "findings": [
+ {
+ "code": "duplicate-credit-claim",
+ "severity": "hold",
+ "message": "Another claim uses the same contributor, role, project, and evidence set.",
+ "remediation": "Deduplicate before computing profile reputation deltas."
+ }
+ ]
+ },
+ {
+ "claimId": "claim-007",
+ "contributorId": "user-otto",
+ "contributorName": "Otto Gray",
+ "projectId": "proj-neuro-open-001",
+ "role": "Validation",
+ "decision": "reject",
+ "riskScore": 8,
+ "evidenceIds": [],
+ "findings": [
+ {
+ "code": "empty-evidence-set",
+ "severity": "reject",
+ "message": "No artifact evidence is attached to the contribution claim.",
+ "remediation": "Attach at least one artifact matching the claimed role."
+ }
+ ]
+ }
+ ]
+}
diff --git a/credit-role-evidence-guard/reports/reviewer-report.md b/credit-role-evidence-guard/reports/reviewer-report.md
new file mode 100644
index 00000000..cd319e35
--- /dev/null
+++ b/credit-role-evidence-guard/reports/reviewer-report.md
@@ -0,0 +1,71 @@
+# Credit Role Evidence Consistency Report
+
+Generated: 2026-05-22T12:00:00.000Z
+
+## Summary
+
+- Approved: 1
+- Held for steward review: 4
+- Rejected before publication: 2
+- Findings: 7
+- Aggregate risk score: 31
+
+## Decisions
+
+### claim-001: HOLD
+
+- Contributor: Mei Lin (user-mei)
+- Project: proj-neuro-open-001
+- Role: Data curation
+- Risk score: 3
+- duplicate-credit-claim: Another claim uses the same contributor, role, project, and evidence set. Remediation: Deduplicate before computing profile reputation deltas.
+
+### claim-002: APPROVE
+
+- Contributor: Nora Hart (user-nora)
+- Project: proj-climate-model-014
+- Role: Software
+- Risk score: 0
+- Findings: none
+
+### claim-003: REJECT
+
+- Contributor: Ada Pike (user-ada)
+- Project: proj-neuro-open-001
+- Role: Peer review
+- Risk score: 11
+- author-review-credit-conflict: A project author cannot receive peer-review reputation credit for reviewing their own project. Remediation: Route the review to an independent reviewer or record it as author response work.
+- blind-review-identity-leak: Public profile credit is backed by blind-review evidence that could expose reviewer identity. Remediation: Publish an anonymized receipt or keep the credit private until the review mode permits disclosure.
+
+### claim-004: HOLD
+
+- Contributor: Ravi Singh (user-ravi)
+- Project: proj-climate-model-014
+- Role: Software
+- Risk score: 3
+- role-evidence-mismatch: Role "Software" expects code, test, notebook evidence, but got manuscript. Remediation: Attach role-appropriate evidence or change the claimed role.
+
+### claim-005: HOLD
+
+- Contributor: Lena Rowe (user-lena)
+- Project: proj-climate-model-014
+- Role: Writing - original draft
+- Risk score: 3
+- missing-profile-consent: Lena Rowe has not consented to publish this credit in institutional-export. Remediation: Collect contributor consent or keep the credit internal.
+
+### claim-006: HOLD
+
+- Contributor: Mei Lin (user-mei)
+- Project: proj-neuro-open-001
+- Role: Data curation
+- Risk score: 3
+- duplicate-credit-claim: Another claim uses the same contributor, role, project, and evidence set. Remediation: Deduplicate before computing profile reputation deltas.
+
+### claim-007: REJECT
+
+- Contributor: Otto Gray (user-otto)
+- Project: proj-neuro-open-001
+- Role: Validation
+- Risk score: 8
+- empty-evidence-set: No artifact evidence is attached to the contribution claim. Remediation: Attach at least one artifact matching the claimed role.
+
diff --git a/credit-role-evidence-guard/reports/summary.svg b/credit-role-evidence-guard/reports/summary.svg
new file mode 100644
index 00000000..0afa7a94
--- /dev/null
+++ b/credit-role-evidence-guard/reports/summary.svg
@@ -0,0 +1,13 @@
+
diff --git a/credit-role-evidence-guard/sample-data.js b/credit-role-evidence-guard/sample-data.js
new file mode 100644
index 00000000..14c2431b
--- /dev/null
+++ b/credit-role-evidence-guard/sample-data.js
@@ -0,0 +1,152 @@
+export const sampleProjects = [
+ {
+ id: "proj-neuro-open-001",
+ title: "Open EEG Protocol for Sleep Stage Reproducibility",
+ authors: ["user-ada", "user-mei"],
+ maintainers: ["user-ada"],
+ },
+ {
+ id: "proj-climate-model-014",
+ title: "Regional Climate Downscaling Benchmark",
+ authors: ["user-ravi"],
+ maintainers: ["user-ravi", "user-nora"],
+ },
+];
+
+export const sampleArtifacts = [
+ {
+ id: "artifact-protocol-001",
+ projectId: "proj-neuro-open-001",
+ type: "protocol",
+ path: "protocols/sleep-stage-eeg.md",
+ createdAt: "2026-04-10T09:00:00Z",
+ hash: "sha256:9d6c5c01b8d8848e2c24a7e7c7d5e6c2",
+ visibility: "public",
+ },
+ {
+ id: "artifact-dataset-raw-001",
+ projectId: "proj-neuro-open-001",
+ type: "dataset",
+ path: "data/eeg-nightly-windowed.parquet",
+ createdAt: "2026-04-12T10:30:00Z",
+ hash: "sha256:4c4ddcbe79c3f3eaa084bf0d977cf062",
+ visibility: "restricted",
+ },
+ {
+ id: "artifact-data-dictionary-001",
+ projectId: "proj-neuro-open-001",
+ type: "data-dictionary",
+ path: "data/dictionary.json",
+ createdAt: "2026-04-13T16:20:00Z",
+ hash: "sha256:20ce9fa2f3d93c6e29af6ed21b561d44",
+ visibility: "public",
+ },
+ {
+ id: "artifact-analysis-code-001",
+ projectId: "proj-climate-model-014",
+ type: "code",
+ path: "code/downscale.py",
+ createdAt: "2026-04-18T15:40:00Z",
+ hash: "sha256:f79dd255f8fdd873bc36e4aa2be81f31",
+ visibility: "public",
+ },
+ {
+ id: "artifact-review-blind-001",
+ projectId: "proj-neuro-open-001",
+ type: "peer-review",
+ path: "reviews/blind-round-1/review-a.md",
+ createdAt: "2026-04-21T13:45:00Z",
+ hash: "sha256:0aee44d2fde3cb9af2a735667c1f41d7",
+ visibility: "blind-review",
+ },
+ {
+ id: "artifact-manuscript-001",
+ projectId: "proj-climate-model-014",
+ type: "manuscript",
+ path: "manuscript/main.md",
+ createdAt: "2026-04-20T10:00:00Z",
+ hash: "sha256:3869e11a11e16d7a6ad65fc0414de8ef",
+ visibility: "public",
+ },
+];
+
+export const sampleCreditClaims = [
+ {
+ id: "claim-001",
+ projectId: "proj-neuro-open-001",
+ contributorId: "user-mei",
+ contributorName: "Mei Lin",
+ role: "Data curation",
+ evidenceIds: ["artifact-dataset-raw-001", "artifact-data-dictionary-001"],
+ profileVisibility: "public",
+ consentGranted: true,
+ declaredAt: "2026-04-15T11:00:00Z",
+ },
+ {
+ id: "claim-002",
+ projectId: "proj-climate-model-014",
+ contributorId: "user-nora",
+ contributorName: "Nora Hart",
+ role: "Software",
+ evidenceIds: ["artifact-analysis-code-001"],
+ profileVisibility: "public",
+ consentGranted: true,
+ declaredAt: "2026-04-19T10:15:00Z",
+ },
+ {
+ id: "claim-003",
+ projectId: "proj-neuro-open-001",
+ contributorId: "user-ada",
+ contributorName: "Ada Pike",
+ role: "Peer review",
+ evidenceIds: ["artifact-review-blind-001"],
+ profileVisibility: "public",
+ consentGranted: true,
+ declaredAt: "2026-04-21T16:10:00Z",
+ },
+ {
+ id: "claim-004",
+ projectId: "proj-climate-model-014",
+ contributorId: "user-ravi",
+ contributorName: "Ravi Singh",
+ role: "Software",
+ evidenceIds: ["artifact-manuscript-001"],
+ profileVisibility: "public",
+ consentGranted: true,
+ declaredAt: "2026-04-21T08:00:00Z",
+ },
+ {
+ id: "claim-005",
+ projectId: "proj-climate-model-014",
+ contributorId: "user-lena",
+ contributorName: "Lena Rowe",
+ role: "Writing - original draft",
+ evidenceIds: ["artifact-manuscript-001"],
+ profileVisibility: "institutional-export",
+ consentGranted: false,
+ declaredAt: "2026-04-23T12:20:00Z",
+ },
+ {
+ id: "claim-006",
+ projectId: "proj-neuro-open-001",
+ contributorId: "user-mei",
+ contributorName: "Mei Lin",
+ role: "Data curation",
+ evidenceIds: ["artifact-dataset-raw-001", "artifact-data-dictionary-001"],
+ profileVisibility: "public",
+ consentGranted: true,
+ declaredAt: "2026-04-15T11:05:00Z",
+ },
+ {
+ id: "claim-007",
+ projectId: "proj-neuro-open-001",
+ contributorId: "user-otto",
+ contributorName: "Otto Gray",
+ role: "Validation",
+ evidenceIds: [],
+ profileVisibility: "public",
+ consentGranted: true,
+ declaredAt: "2026-05-20T09:00:00Z",
+ },
+];
+
diff --git a/credit-role-evidence-guard/test.js b/credit-role-evidence-guard/test.js
new file mode 100644
index 00000000..ee043d49
--- /dev/null
+++ b/credit-role-evidence-guard/test.js
@@ -0,0 +1,55 @@
+import assert from "node:assert/strict";
+import { evaluateCreditClaims } from "./index.js";
+import { sampleArtifacts, sampleCreditClaims, sampleProjects } from "./sample-data.js";
+
+const result = evaluateCreditClaims({
+ projects: sampleProjects,
+ artifacts: sampleArtifacts,
+ claims: sampleCreditClaims,
+});
+
+assert.equal(result.summary.approve, 1, "only the software claim should approve cleanly");
+assert.equal(result.summary.hold, 4, "four claims should be held for steward review");
+assert.equal(result.summary.reject, 2, "two claims should be rejected before publication");
+
+const byClaim = new Map(result.decisions.map((decision) => [decision.claimId, decision]));
+
+assert.equal(byClaim.get("claim-001").decision, "hold");
+assert.ok(
+ byClaim.get("claim-001").findings.some((finding) => finding.code === "duplicate-credit-claim"),
+ "duplicate data curation claim should be held",
+);
+
+assert.equal(byClaim.get("claim-002").decision, "approve");
+assert.deepEqual(byClaim.get("claim-002").findings, []);
+
+assert.equal(byClaim.get("claim-003").decision, "reject");
+assert.ok(
+ byClaim.get("claim-003").findings.some((finding) => finding.code === "author-review-credit-conflict"),
+ "author peer-review self-credit should reject",
+);
+assert.ok(
+ byClaim.get("claim-003").findings.some((finding) => finding.code === "blind-review-identity-leak"),
+ "blind-review evidence should not publish directly to a public profile",
+);
+
+assert.equal(byClaim.get("claim-004").decision, "hold");
+assert.ok(
+ byClaim.get("claim-004").findings.some((finding) => finding.code === "role-evidence-mismatch"),
+ "software credit backed by manuscript evidence should be held",
+);
+
+assert.equal(byClaim.get("claim-005").decision, "hold");
+assert.ok(
+ byClaim.get("claim-005").findings.some((finding) => finding.code === "missing-profile-consent"),
+ "institutional export without contributor consent should be held",
+);
+
+assert.equal(byClaim.get("claim-007").decision, "reject");
+assert.ok(
+ byClaim.get("claim-007").findings.some((finding) => finding.code === "empty-evidence-set"),
+ "validation claim without evidence should reject",
+);
+
+console.log("credit-role-evidence-guard tests passed");
+
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..bafd860f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+ "scripts": {
+ "check": "node --check credit-role-evidence-guard/index.js && node --check credit-role-evidence-guard/sample-data.js && node --check credit-role-evidence-guard/test.js && node --check credit-role-evidence-guard/demo.js",
+ "test": "node credit-role-evidence-guard/test.js",
+ "demo": "node credit-role-evidence-guard/demo.js"
+ },
+ "type": "module"
+}