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 ` + + Credit Role Evidence Guard + CRediT claims are checked before reputation and profile publication. + + + + Approved: ${approved} + Steward review: ${held} + Rejected: ${rejected} + Findings: ${result.summary.totalFindings} | Aggregate risk score: ${result.summary.riskScore} + Synthetic sample data only. No external APIs, private profiles, credentials, or live users. + +`; +} + 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 @@ + + + Credit Role Evidence Guard + CRediT claims are checked before reputation and profile publication. + + + + Approved: 1 + Steward review: 4 + Rejected: 2 + Findings: 7 | Aggregate risk score: 31 + Synthetic sample data only. No external APIs, private profiles, credentials, or live users. + 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" +}