diff --git a/reproducibility-badge-verification-gate/README.md b/reproducibility-badge-verification-gate/README.md
new file mode 100644
index 00000000..4ca4229a
--- /dev/null
+++ b/reproducibility-badge-verification-gate/README.md
@@ -0,0 +1,24 @@
+# Reproducibility Badge Verification Gate
+
+Self-contained community and reputation module for SCIBASE issue #15.
+
+The gate evaluates whether a project should receive a primary reproducibility badge based on independent reproduction evidence. It checks required artifact hashes, executable environment digests, output matches, test results, run-log checksums, conflict disclosures, author/collaborator conflicts, and the number of valid independent reproduction runs required for bronze, silver, or gold badges.
+
+## Run
+
+```bash
+npm run check
+npm test
+npm run demo
+```
+
+The demo writes reviewer artifacts to `reports/`:
+
+- `reproducibility-badge-packet.json`
+- `reproducibility-badge-report.md`
+- `summary.svg`
+- `demo.mp4`
+
+## Scope
+
+This slice is intentionally narrow. It does not implement leaderboards, badge renewal, endorsement rings, review civility, review timeliness credits, mentorship scoring, anonymous identity escrow, or broad reputation transparency receipts. It focuses on primary badge issuance from independently verified reproducibility evidence.
diff --git a/reproducibility-badge-verification-gate/acceptance-notes.md b/reproducibility-badge-verification-gate/acceptance-notes.md
new file mode 100644
index 00000000..dbdb38e4
--- /dev/null
+++ b/reproducibility-badge-verification-gate/acceptance-notes.md
@@ -0,0 +1,10 @@
+# Acceptance Notes
+
+- Dependency-free Node.js module with deterministic output.
+- Synthetic data only; no private identities, credentials, payment data, or external services.
+- Badge decisions are `pass`, `revise`, or `hold`.
+- `pass` means the requested badge level has enough independent successful reproduction evidence.
+- `revise` means evidence may support a lower badge or needs more non-blocking disclosure.
+- `hold` means no badge should be issued until blocker findings are fixed.
+- Blockers cover conflicted reproducers, missing artifact checksums, environment/output mismatches, failed tests, missing reproducer disclosures, unknown artifacts, unsupported roles, and too few independent runs.
+- Demo artifacts are generated locally under `reports/`.
diff --git a/reproducibility-badge-verification-gate/demo.js b/reproducibility-badge-verification-gate/demo.js
new file mode 100644
index 00000000..ff8c37cb
--- /dev/null
+++ b/reproducibility-badge-verification-gate/demo.js
@@ -0,0 +1,83 @@
+const fs = require("node:fs")
+const path = require("node:path")
+const { spawnSync } = require("node:child_process")
+const { evaluateBadgePortfolio } = require("./index")
+const { badgePolicy, projects } = require("./sample-data")
+
+const reportsDir = path.join(__dirname, "reports")
+fs.mkdirSync(reportsDir, { recursive: true })
+
+const packet = evaluateBadgePortfolio({ projects, policy: badgePolicy })
+const { summary } = packet
+
+fs.writeFileSync(
+ path.join(reportsDir, "reproducibility-badge-packet.json"),
+ `${JSON.stringify(packet, null, 2)}\n`,
+)
+
+const markdown = [
+ "# Reproducibility Badge Verification Report",
+ "",
+ `Generated projects: ${summary.totalProjects}`,
+ `Passed: ${summary.passed}`,
+ `Needs revision: ${summary.revise}`,
+ `Held: ${summary.held}`,
+ `Valid independent runs: ${summary.validIndependentRuns}`,
+ `Audit digest: \`${packet.audit.digest}\``,
+ "",
+ "## Badge Decisions",
+ ...packet.decisions.flatMap((decision) => [
+ "",
+ `### ${decision.id}: ${decision.title}`,
+ `- Status: ${decision.status}`,
+ `- Requested badge: ${decision.requestedBadge}`,
+ `- Recommended badge: ${decision.recommendedBadge}`,
+ `- Required artifacts: ${decision.evidence.requiredArtifacts}`,
+ `- Reproduction runs: ${decision.evidence.reproductionRuns}`,
+ `- Valid independent runs: ${decision.evidence.validIndependentRuns}`,
+ `- Unique valid institutions: ${decision.evidence.uniqueValidInstitutions}`,
+ `- Findings: ${decision.findings.map((finding) => finding.code).join(", ") || "none"}`,
+ `- First action: ${decision.reviewerActions[0]?.message || "none"}`,
+ ]),
+ "",
+]
+
+fs.writeFileSync(path.join(reportsDir, "reproducibility-badge-report.md"), markdown.join("\n"))
+
+const svg = `
+`
+fs.writeFileSync(path.join(reportsDir, "summary.svg"), svg)
+
+const ffmpeg = spawnSync("ffmpeg", [
+ "-y",
+ "-f",
+ "lavfi",
+ "-i",
+ "color=c=0x111827:s=960x540:d=6:r=15",
+ "-vf",
+ "drawbox=x=48:y=170:w=250:h=150:color=0x15803d@1:t=fill,drawbox=x=355:y=170:w=250:h=150:color=0xb45309@1:t=fill,drawbox=x=662:y=170:w=250:h=150:color=0xbe123c@1:t=fill,drawbox=x=48:y=370:w=864:h=18:color=0x22c55e@1:t=fill",
+ "-pix_fmt",
+ "yuv420p",
+ path.join(reportsDir, "demo.mp4"),
+], { stdio: "ignore" })
+
+if (ffmpeg.status !== 0) {
+ console.warn("ffmpeg video generation failed; summary.svg and JSON/Markdown reports were still generated.")
+}
+
+console.log(`Wrote reproducibility badge artifacts to ${reportsDir}`)
diff --git a/reproducibility-badge-verification-gate/index.js b/reproducibility-badge-verification-gate/index.js
new file mode 100644
index 00000000..e529ca98
--- /dev/null
+++ b/reproducibility-badge-verification-gate/index.js
@@ -0,0 +1,326 @@
+const crypto = require("node:crypto")
+
+function stableJson(value) {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableJson).join(",")}]`
+ }
+ if (value && typeof value === "object") {
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`
+ }
+ return JSON.stringify(value)
+}
+
+function digestFor(value) {
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex")
+}
+
+function finding(code, severity, message, detail = {}) {
+ return { code, severity, message, detail }
+}
+
+function hasText(value) {
+ return typeof value === "string" && value.trim().length > 0
+}
+
+function unique(values) {
+ return [...new Set(values)]
+}
+
+function countDaysBetween(a, b) {
+ const start = new Date(a)
+ const end = new Date(b)
+ return Math.floor((end.getTime() - start.getTime()) / 86400000)
+}
+
+function evaluateArtifactManifest(project, policy) {
+ const findings = []
+ const artifactIds = new Set(project.artifacts.map((artifact) => artifact.id))
+
+ project.artifacts.forEach((artifact) => {
+ if (artifact.required && !hasText(artifact.checksum)) {
+ findings.push(finding("ARTIFACT_CHECKSUM_MISSING", "blocker", "Required reproducibility artifact is missing a stable checksum.", {
+ artifactId: artifact.id,
+ }))
+ }
+ if (artifact.required && !hasText(artifact.version)) {
+ findings.push(finding("ARTIFACT_VERSION_MISSING", "warning", "Required artifact should include a version or release tag.", {
+ artifactId: artifact.id,
+ }))
+ }
+ })
+
+ project.reproductionRuns.forEach((run) => {
+ run.artifactChecks.forEach((check) => {
+ if (!artifactIds.has(check.artifactId)) {
+ findings.push(finding("UNKNOWN_ARTIFACT_CHECK", "blocker", "Reproduction run references an artifact outside the badge manifest.", {
+ runId: run.id,
+ artifactId: check.artifactId,
+ }))
+ }
+ })
+ })
+
+ return findings
+}
+
+function evaluateEnvironmentManifest(project) {
+ const findings = []
+
+ project.environments.forEach((environment) => {
+ if (!hasText(environment.digest)) {
+ findings.push(finding("ENVIRONMENT_DIGEST_MISSING", "blocker", "Executable environment is missing a reproducible digest.", {
+ environmentId: environment.id,
+ }))
+ }
+ if (!hasText(environment.lockfileChecksum)) {
+ findings.push(finding("LOCKFILE_CHECKSUM_MISSING", "warning", "Environment lockfile checksum is missing.", {
+ environmentId: environment.id,
+ }))
+ }
+ })
+
+ return findings
+}
+
+function evaluateRunIndependence(run, project, policy) {
+ const findings = []
+ const authorIds = new Set(project.authors.map((author) => author.id))
+ const authorInstitutions = new Set(project.authors.map((author) => author.institution))
+
+ if (authorIds.has(run.reproducerId) || run.relatedAuthorIds.length > 0) {
+ findings.push(finding("REPRODUCER_AUTHOR_CONFLICT", "blocker", "Badge verification run is not independent from project authors.", {
+ runId: run.id,
+ reproducerId: run.reproducerId,
+ relatedAuthorIds: run.relatedAuthorIds,
+ }))
+ }
+ if (run.recentCollaboratorAuthorIds.length > 0) {
+ findings.push(finding("RECENT_COLLABORATOR_CONFLICT", "blocker", "Recent collaborator cannot count as an independent badge reproducer.", {
+ runId: run.id,
+ relatedAuthorIds: run.recentCollaboratorAuthorIds,
+ }))
+ }
+ if (run.fundingRelationship) {
+ findings.push(finding("FUNDER_RELATIONSHIP_DISCLOSURE", "warning", "Funding relationship should be disclosed before counting reputation credit.", {
+ runId: run.id,
+ }))
+ }
+ if (authorInstitutions.has(run.institution)) {
+ findings.push(finding("SAME_INSTITUTION_REVIEW", "warning", "Same-institution reproduction should be disclosed and normally paired with another independent run.", {
+ runId: run.id,
+ institution: run.institution,
+ }))
+ }
+ if (!policy.allowedReproducerRoles.includes(run.role)) {
+ findings.push(finding("REPRODUCER_ROLE_NOT_ALLOWED", "blocker", "Reproducer role is not allowed to verify a badge.", {
+ runId: run.id,
+ role: run.role,
+ }))
+ }
+
+ return findings
+}
+
+function evaluateRunEvidence(run, project, policy) {
+ const findings = []
+ const requiredArtifacts = project.artifacts.filter((artifact) => artifact.required)
+ const matchedRequired = requiredArtifacts.filter((artifact) => {
+ return run.artifactChecks.some((check) => check.artifactId === artifact.id && check.checksumMatches)
+ })
+ const coverageRatio = requiredArtifacts.length === 0 ? 0 : Number((matchedRequired.length / requiredArtifacts.length).toFixed(3))
+ const ageDays = countDaysBetween(run.completedAt, project.badgeRequest.reviewedAt)
+
+ if (coverageRatio < policy.minArtifactCoverage) {
+ findings.push(finding("ARTIFACT_COVERAGE_INCOMPLETE", "blocker", "Reproduction run did not verify all required artifacts.", {
+ runId: run.id,
+ coverageRatio,
+ }))
+ }
+ if (!run.environmentDigestMatches) {
+ findings.push(finding("ENVIRONMENT_DIGEST_MISMATCH", "blocker", "Reproduction run used an environment digest that does not match the badge manifest.", {
+ runId: run.id,
+ }))
+ }
+ if (!run.outputDigestMatches) {
+ findings.push(finding("OUTPUT_DIGEST_MISMATCH", "blocker", "Reproduced outputs do not match the submitted result digest.", {
+ runId: run.id,
+ }))
+ }
+ if (!run.testsPassed) {
+ findings.push(finding("REPRODUCTION_TESTS_FAILED", "blocker", "Reproduction tests failed for the submitted project.", {
+ runId: run.id,
+ }))
+ }
+ if (!hasText(run.runLogChecksum)) {
+ findings.push(finding("RUN_LOG_CHECKSUM_MISSING", "warning", "Reproduction run log should include a checksum for auditability.", {
+ runId: run.id,
+ }))
+ }
+ if (!run.disclosureSigned) {
+ findings.push(finding("REPRODUCER_DISCLOSURE_MISSING", "blocker", "Independent reproducer must sign the conflict and method disclosure.", {
+ runId: run.id,
+ }))
+ }
+ if (ageDays > policy.maxRunAgeDays) {
+ findings.push(finding("REPRODUCTION_RUN_STALE", "warning", "Reproduction run is older than the badge review freshness window.", {
+ runId: run.id,
+ ageDays,
+ }))
+ }
+
+ return findings
+}
+
+function runHasBlocker(findings) {
+ return findings.some((item) => item.severity === "blocker")
+}
+
+function badgeLevelForValidRuns(validRuns, policy) {
+ const levels = Object.entries(policy.minIndependentRunsByBadge)
+ return levels
+ .filter(([, count]) => validRuns >= count)
+ .sort((a, b) => b[1] - a[1])[0]?.[0] || "none"
+}
+
+function buildReviewerActions(status, findings, requestedBadge, recommendedBadge) {
+ if (status === "pass") {
+ return [{ code: "ISSUE_REPRODUCIBILITY_BADGE", owner: "community-moderator", message: `Issue the requested ${requestedBadge} reproducibility badge.` }]
+ }
+ if (status === "revise") {
+ return [{ code: "ISSUE_LOWER_BADGE_OR_COLLECT_EVIDENCE", owner: "community-moderator", message: `Requested ${requestedBadge} badge is not fully supported; consider ${recommendedBadge} or collect more evidence.` }]
+ }
+
+ const actions = findings.map((item) => {
+ if (item.code === "REPRODUCER_AUTHOR_CONFLICT" || item.code === "RECENT_COLLABORATOR_CONFLICT") {
+ return { code: "REPLACE_CONFLICTED_REPRODUCER", owner: "community-moderator", message: "Request a new independent reproducer before issuing reputation credit." }
+ }
+ if (item.code === "ARTIFACT_COVERAGE_INCOMPLETE" || item.code === "ARTIFACT_CHECKSUM_MISSING") {
+ return { code: "FIX_REPRODUCIBILITY_MANIFEST", owner: "project-author", message: "Complete artifact checksums and coverage before badge review." }
+ }
+ if (item.code === "ENVIRONMENT_DIGEST_MISMATCH" || item.code === "OUTPUT_DIGEST_MISMATCH" || item.code === "REPRODUCTION_TESTS_FAILED") {
+ return { code: "RERUN_REPRODUCTION_EVIDENCE", owner: "independent-reproducer", message: "Re-run reproduction until environment, outputs, and tests match the submitted evidence." }
+ }
+ if (item.code === "REPRODUCER_DISCLOSURE_MISSING") {
+ return { code: "COLLECT_REPRODUCER_DISCLOSURE", owner: "community-moderator", message: "Collect conflict and method disclosure before counting the run." }
+ }
+ return { code: `ADDRESS_${item.code}`, owner: "community-moderator", message: item.message }
+ })
+
+ return [...new Map(actions.map((item) => [item.code, item])).values()]
+}
+
+function evaluateProjectBadge(project, policy) {
+ const manifestFindings = [
+ ...evaluateArtifactManifest(project, policy),
+ ...evaluateEnvironmentManifest(project),
+ ]
+
+ const runDecisions = project.reproductionRuns.map((run) => {
+ const findings = [
+ ...evaluateRunIndependence(run, project, policy),
+ ...evaluateRunEvidence(run, project, policy),
+ ]
+ const independent = !runHasBlocker(findings)
+ return {
+ id: run.id,
+ reproducerId: run.reproducerId,
+ role: run.role,
+ institution: run.institution,
+ independent,
+ findings,
+ auditDigest: digestFor({ run, findings, independent }),
+ }
+ })
+
+ const validRuns = runDecisions.filter((run) => run.independent)
+ const requestedBadge = project.badgeRequest.level
+ const requestedRunCount = policy.minIndependentRunsByBadge[requestedBadge] || Number.POSITIVE_INFINITY
+ const recommendedBadge = badgeLevelForValidRuns(validRuns.length, policy)
+ const badgeFindings = [...manifestFindings, ...runDecisions.flatMap((run) => run.findings)]
+ const uniqueInstitutions = unique(validRuns.map((run) => run.institution))
+
+ if (!policy.badgeLevels.includes(requestedBadge)) {
+ badgeFindings.push(finding("BADGE_LEVEL_UNKNOWN", "blocker", "Requested badge level is not recognized.", {
+ requestedBadge,
+ }))
+ }
+ if (validRuns.length < requestedRunCount) {
+ badgeFindings.push(finding("INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD", "blocker", "Not enough independent successful reproduction runs for the requested badge.", {
+ requestedBadge,
+ validRuns: validRuns.length,
+ requiredRuns: requestedRunCount,
+ }))
+ }
+ if (validRuns.length >= 2 && uniqueInstitutions.length < policy.minInstitutionsForMultiRunBadge) {
+ badgeFindings.push(finding("INSTITUTION_DIVERSITY_LOW", "warning", "Multiple reproduction runs should come from more than one institution when possible.", {
+ institutions: uniqueInstitutions,
+ }))
+ }
+
+ const blockers = badgeFindings.filter((item) => item.severity === "blocker")
+ const warnings = badgeFindings.filter((item) => item.severity === "warning")
+ let status = "pass"
+ if (blockers.length > 0 && recommendedBadge !== "none" && policy.allowLowerBadgeRecommendation) {
+ status = "revise"
+ } else if (blockers.length > 0) {
+ status = "hold"
+ } else if (warnings.length > 0) {
+ status = "revise"
+ }
+
+ const decision = {
+ id: project.id,
+ title: project.title,
+ status,
+ requestedBadge,
+ recommendedBadge,
+ evidence: {
+ artifacts: project.artifacts.length,
+ requiredArtifacts: project.artifacts.filter((artifact) => artifact.required).length,
+ environments: project.environments.length,
+ reproductionRuns: project.reproductionRuns.length,
+ validIndependentRuns: validRuns.length,
+ uniqueValidInstitutions: uniqueInstitutions.length,
+ },
+ findings: badgeFindings,
+ runDecisions,
+ reviewerActions: buildReviewerActions(status, badgeFindings, requestedBadge, recommendedBadge),
+ }
+
+ return {
+ ...decision,
+ auditDigest: digestFor(decision),
+ }
+}
+
+function evaluateBadgePortfolio({ projects, policy }) {
+ const decisions = projects.map((project) => evaluateProjectBadge(project, policy))
+ const summary = {
+ totalProjects: decisions.length,
+ passed: decisions.filter((decision) => decision.status === "pass").length,
+ revise: decisions.filter((decision) => decision.status === "revise").length,
+ held: decisions.filter((decision) => decision.status === "hold").length,
+ requestedBadges: decisions.reduce((acc, decision) => {
+ acc[decision.requestedBadge] = (acc[decision.requestedBadge] || 0) + 1
+ return acc
+ }, {}),
+ validIndependentRuns: decisions.reduce((sum, decision) => sum + decision.evidence.validIndependentRuns, 0),
+ }
+
+ return {
+ generatedAt: "2026-05-21T18:35:00.000Z",
+ policy,
+ summary,
+ decisions,
+ audit: {
+ source: "synthetic-reproducibility-badge-verification-gate",
+ digest: digestFor({ summary, decisions }),
+ },
+ }
+}
+
+module.exports = {
+ digestFor,
+ evaluateBadgePortfolio,
+ evaluateProjectBadge,
+}
diff --git a/reproducibility-badge-verification-gate/package.json b/reproducibility-badge-verification-gate/package.json
new file mode 100644
index 00000000..7b2519ea
--- /dev/null
+++ b/reproducibility-badge-verification-gate/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "reproducibility-badge-verification-gate",
+ "version": "1.0.0",
+ "private": true,
+ "type": "commonjs",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js",
+ "test": "node test.js",
+ "demo": "node demo.js"
+ }
+}
diff --git a/reproducibility-badge-verification-gate/reports/demo.mp4 b/reproducibility-badge-verification-gate/reports/demo.mp4
new file mode 100644
index 00000000..c7efd0df
Binary files /dev/null and b/reproducibility-badge-verification-gate/reports/demo.mp4 differ
diff --git a/reproducibility-badge-verification-gate/reports/reproducibility-badge-packet.json b/reproducibility-badge-verification-gate/reports/reproducibility-badge-packet.json
new file mode 100644
index 00000000..f3998c48
--- /dev/null
+++ b/reproducibility-badge-verification-gate/reports/reproducibility-badge-packet.json
@@ -0,0 +1,495 @@
+{
+ "generatedAt": "2026-05-21T18:35:00.000Z",
+ "policy": {
+ "badgeLevels": [
+ "bronze",
+ "silver",
+ "gold"
+ ],
+ "minIndependentRunsByBadge": {
+ "bronze": 1,
+ "silver": 2,
+ "gold": 3
+ },
+ "allowedReproducerRoles": [
+ "independent-reproducer",
+ "community-reviewer",
+ "external-methods-reviewer"
+ ],
+ "minArtifactCoverage": 1,
+ "minInstitutionsForMultiRunBadge": 2,
+ "maxRunAgeDays": 90,
+ "allowLowerBadgeRecommendation": true
+ },
+ "summary": {
+ "totalProjects": 4,
+ "passed": 1,
+ "revise": 1,
+ "held": 2,
+ "requestedBadges": {
+ "silver": 2,
+ "bronze": 2
+ },
+ "validIndependentRuns": 3
+ },
+ "decisions": [
+ {
+ "id": "RB-PASS-001",
+ "title": "Open enzyme model reproducibility packet",
+ "status": "pass",
+ "requestedBadge": "silver",
+ "recommendedBadge": "silver",
+ "evidence": {
+ "artifacts": 3,
+ "requiredArtifacts": 3,
+ "environments": 1,
+ "reproductionRuns": 2,
+ "validIndependentRuns": 2,
+ "uniqueValidInstitutions": 2
+ },
+ "findings": [],
+ "runDecisions": [
+ {
+ "id": "run-a",
+ "reproducerId": "rep-101",
+ "role": "independent-reproducer",
+ "institution": "West Methods Institute",
+ "independent": true,
+ "findings": [],
+ "auditDigest": "46f3325378dfe3912b446873ff880984625efb9e12df3fc55fa665ac3fee7c35"
+ },
+ {
+ "id": "run-b",
+ "reproducerId": "rep-202",
+ "role": "external-methods-reviewer",
+ "institution": "South Reproducibility Center",
+ "independent": true,
+ "findings": [],
+ "auditDigest": "161ce2532a735ae4f596046cbb699342d919b7dc2c7c4fea52ad5f7f1e563822"
+ }
+ ],
+ "reviewerActions": [
+ {
+ "code": "ISSUE_REPRODUCIBILITY_BADGE",
+ "owner": "community-moderator",
+ "message": "Issue the requested silver reproducibility badge."
+ }
+ ],
+ "auditDigest": "f881891e8522aeb763ddf20fa922fc8202633e494b3a22073d4bf94817d5f2df"
+ },
+ {
+ "id": "RB-REV-002",
+ "title": "Ecology sampling reproducibility packet",
+ "status": "revise",
+ "requestedBadge": "silver",
+ "recommendedBadge": "bronze",
+ "evidence": {
+ "artifacts": 2,
+ "requiredArtifacts": 2,
+ "environments": 1,
+ "reproductionRuns": 1,
+ "validIndependentRuns": 1,
+ "uniqueValidInstitutions": 1
+ },
+ "findings": [
+ {
+ "code": "INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD",
+ "severity": "blocker",
+ "message": "Not enough independent successful reproduction runs for the requested badge.",
+ "detail": {
+ "requestedBadge": "silver",
+ "validRuns": 1,
+ "requiredRuns": 2
+ }
+ }
+ ],
+ "runDecisions": [
+ {
+ "id": "run-c",
+ "reproducerId": "rep-303",
+ "role": "independent-reproducer",
+ "institution": "Mountain Stats Lab",
+ "independent": true,
+ "findings": [],
+ "auditDigest": "a89b1d90aac1a0eef6d8af771bfc5ccc908953bf35d29821c7005cf35098fd8e"
+ }
+ ],
+ "reviewerActions": [
+ {
+ "code": "ISSUE_LOWER_BADGE_OR_COLLECT_EVIDENCE",
+ "owner": "community-moderator",
+ "message": "Requested silver badge is not fully supported; consider bronze or collect more evidence."
+ }
+ ],
+ "auditDigest": "99c8cfb891f9d1fda3a3ade460ec19de7a2f98226ae8a1caffa5323d8382aa2e"
+ },
+ {
+ "id": "RB-HOLD-003",
+ "title": "Conflicted review badge packet",
+ "status": "hold",
+ "requestedBadge": "bronze",
+ "recommendedBadge": "none",
+ "evidence": {
+ "artifacts": 2,
+ "requiredArtifacts": 2,
+ "environments": 1,
+ "reproductionRuns": 2,
+ "validIndependentRuns": 0,
+ "uniqueValidInstitutions": 0
+ },
+ "findings": [
+ {
+ "code": "REPRODUCER_AUTHOR_CONFLICT",
+ "severity": "blocker",
+ "message": "Badge verification run is not independent from project authors.",
+ "detail": {
+ "runId": "run-d",
+ "reproducerId": "author-d",
+ "relatedAuthorIds": [
+ "author-d"
+ ]
+ }
+ },
+ {
+ "code": "SAME_INSTITUTION_REVIEW",
+ "severity": "warning",
+ "message": "Same-institution reproduction should be disclosed and normally paired with another independent run.",
+ "detail": {
+ "runId": "run-d",
+ "institution": "North Lab"
+ }
+ },
+ {
+ "code": "RECENT_COLLABORATOR_CONFLICT",
+ "severity": "blocker",
+ "message": "Recent collaborator cannot count as an independent badge reproducer.",
+ "detail": {
+ "runId": "run-e",
+ "relatedAuthorIds": [
+ "author-e"
+ ]
+ }
+ },
+ {
+ "code": "FUNDER_RELATIONSHIP_DISCLOSURE",
+ "severity": "warning",
+ "message": "Funding relationship should be disclosed before counting reputation credit.",
+ "detail": {
+ "runId": "run-e"
+ }
+ },
+ {
+ "code": "INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD",
+ "severity": "blocker",
+ "message": "Not enough independent successful reproduction runs for the requested badge.",
+ "detail": {
+ "requestedBadge": "bronze",
+ "validRuns": 0,
+ "requiredRuns": 1
+ }
+ }
+ ],
+ "runDecisions": [
+ {
+ "id": "run-d",
+ "reproducerId": "author-d",
+ "role": "community-reviewer",
+ "institution": "North Lab",
+ "independent": false,
+ "findings": [
+ {
+ "code": "REPRODUCER_AUTHOR_CONFLICT",
+ "severity": "blocker",
+ "message": "Badge verification run is not independent from project authors.",
+ "detail": {
+ "runId": "run-d",
+ "reproducerId": "author-d",
+ "relatedAuthorIds": [
+ "author-d"
+ ]
+ }
+ },
+ {
+ "code": "SAME_INSTITUTION_REVIEW",
+ "severity": "warning",
+ "message": "Same-institution reproduction should be disclosed and normally paired with another independent run.",
+ "detail": {
+ "runId": "run-d",
+ "institution": "North Lab"
+ }
+ }
+ ],
+ "auditDigest": "5b7339d275f8ab1301dd54989742e3f24a94017b03086184e6450003ef4bfe7a"
+ },
+ {
+ "id": "run-e",
+ "reproducerId": "rep-404",
+ "role": "independent-reproducer",
+ "institution": "East Review Lab",
+ "independent": false,
+ "findings": [
+ {
+ "code": "RECENT_COLLABORATOR_CONFLICT",
+ "severity": "blocker",
+ "message": "Recent collaborator cannot count as an independent badge reproducer.",
+ "detail": {
+ "runId": "run-e",
+ "relatedAuthorIds": [
+ "author-e"
+ ]
+ }
+ },
+ {
+ "code": "FUNDER_RELATIONSHIP_DISCLOSURE",
+ "severity": "warning",
+ "message": "Funding relationship should be disclosed before counting reputation credit.",
+ "detail": {
+ "runId": "run-e"
+ }
+ }
+ ],
+ "auditDigest": "4579095ccfed8d9dfa683b188b1d8c80add9e75c6a2bb84be59eaf52eb3d3b17"
+ }
+ ],
+ "reviewerActions": [
+ {
+ "code": "REPLACE_CONFLICTED_REPRODUCER",
+ "owner": "community-moderator",
+ "message": "Request a new independent reproducer before issuing reputation credit."
+ },
+ {
+ "code": "ADDRESS_SAME_INSTITUTION_REVIEW",
+ "owner": "community-moderator",
+ "message": "Same-institution reproduction should be disclosed and normally paired with another independent run."
+ },
+ {
+ "code": "ADDRESS_FUNDER_RELATIONSHIP_DISCLOSURE",
+ "owner": "community-moderator",
+ "message": "Funding relationship should be disclosed before counting reputation credit."
+ },
+ {
+ "code": "ADDRESS_INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD",
+ "owner": "community-moderator",
+ "message": "Not enough independent successful reproduction runs for the requested badge."
+ }
+ ],
+ "auditDigest": "c8259682193af6c1006544adcc20be9acc2aead0d3c1ca590a8c118e3a4c7943"
+ },
+ {
+ "id": "RB-HOLD-004",
+ "title": "Artifact mismatch badge packet",
+ "status": "hold",
+ "requestedBadge": "bronze",
+ "recommendedBadge": "none",
+ "evidence": {
+ "artifacts": 3,
+ "requiredArtifacts": 3,
+ "environments": 1,
+ "reproductionRuns": 1,
+ "validIndependentRuns": 0,
+ "uniqueValidInstitutions": 0
+ },
+ "findings": [
+ {
+ "code": "ARTIFACT_CHECKSUM_MISSING",
+ "severity": "blocker",
+ "message": "Required reproducibility artifact is missing a stable checksum.",
+ "detail": {
+ "artifactId": "dataset"
+ }
+ },
+ {
+ "code": "ARTIFACT_VERSION_MISSING",
+ "severity": "warning",
+ "message": "Required artifact should include a version or release tag.",
+ "detail": {
+ "artifactId": "results"
+ }
+ },
+ {
+ "code": "ENVIRONMENT_DIGEST_MISSING",
+ "severity": "blocker",
+ "message": "Executable environment is missing a reproducible digest.",
+ "detail": {
+ "environmentId": "container"
+ }
+ },
+ {
+ "code": "LOCKFILE_CHECKSUM_MISSING",
+ "severity": "warning",
+ "message": "Environment lockfile checksum is missing.",
+ "detail": {
+ "environmentId": "container"
+ }
+ },
+ {
+ "code": "ARTIFACT_COVERAGE_INCOMPLETE",
+ "severity": "blocker",
+ "message": "Reproduction run did not verify all required artifacts.",
+ "detail": {
+ "runId": "run-f",
+ "coverageRatio": 0.333
+ }
+ },
+ {
+ "code": "ENVIRONMENT_DIGEST_MISMATCH",
+ "severity": "blocker",
+ "message": "Reproduction run used an environment digest that does not match the badge manifest.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "OUTPUT_DIGEST_MISMATCH",
+ "severity": "blocker",
+ "message": "Reproduced outputs do not match the submitted result digest.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "REPRODUCTION_TESTS_FAILED",
+ "severity": "blocker",
+ "message": "Reproduction tests failed for the submitted project.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "RUN_LOG_CHECKSUM_MISSING",
+ "severity": "warning",
+ "message": "Reproduction run log should include a checksum for auditability.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "REPRODUCER_DISCLOSURE_MISSING",
+ "severity": "blocker",
+ "message": "Independent reproducer must sign the conflict and method disclosure.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD",
+ "severity": "blocker",
+ "message": "Not enough independent successful reproduction runs for the requested badge.",
+ "detail": {
+ "requestedBadge": "bronze",
+ "validRuns": 0,
+ "requiredRuns": 1
+ }
+ }
+ ],
+ "runDecisions": [
+ {
+ "id": "run-f",
+ "reproducerId": "rep-505",
+ "role": "independent-reproducer",
+ "institution": "Open Repro Guild",
+ "independent": false,
+ "findings": [
+ {
+ "code": "ARTIFACT_COVERAGE_INCOMPLETE",
+ "severity": "blocker",
+ "message": "Reproduction run did not verify all required artifacts.",
+ "detail": {
+ "runId": "run-f",
+ "coverageRatio": 0.333
+ }
+ },
+ {
+ "code": "ENVIRONMENT_DIGEST_MISMATCH",
+ "severity": "blocker",
+ "message": "Reproduction run used an environment digest that does not match the badge manifest.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "OUTPUT_DIGEST_MISMATCH",
+ "severity": "blocker",
+ "message": "Reproduced outputs do not match the submitted result digest.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "REPRODUCTION_TESTS_FAILED",
+ "severity": "blocker",
+ "message": "Reproduction tests failed for the submitted project.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "RUN_LOG_CHECKSUM_MISSING",
+ "severity": "warning",
+ "message": "Reproduction run log should include a checksum for auditability.",
+ "detail": {
+ "runId": "run-f"
+ }
+ },
+ {
+ "code": "REPRODUCER_DISCLOSURE_MISSING",
+ "severity": "blocker",
+ "message": "Independent reproducer must sign the conflict and method disclosure.",
+ "detail": {
+ "runId": "run-f"
+ }
+ }
+ ],
+ "auditDigest": "8442560d2273b8b135c84accdb6ea401efd7a18d36d5eda60bc9f29a6f8fe24e"
+ }
+ ],
+ "reviewerActions": [
+ {
+ "code": "FIX_REPRODUCIBILITY_MANIFEST",
+ "owner": "project-author",
+ "message": "Complete artifact checksums and coverage before badge review."
+ },
+ {
+ "code": "ADDRESS_ARTIFACT_VERSION_MISSING",
+ "owner": "community-moderator",
+ "message": "Required artifact should include a version or release tag."
+ },
+ {
+ "code": "ADDRESS_ENVIRONMENT_DIGEST_MISSING",
+ "owner": "community-moderator",
+ "message": "Executable environment is missing a reproducible digest."
+ },
+ {
+ "code": "ADDRESS_LOCKFILE_CHECKSUM_MISSING",
+ "owner": "community-moderator",
+ "message": "Environment lockfile checksum is missing."
+ },
+ {
+ "code": "RERUN_REPRODUCTION_EVIDENCE",
+ "owner": "independent-reproducer",
+ "message": "Re-run reproduction until environment, outputs, and tests match the submitted evidence."
+ },
+ {
+ "code": "ADDRESS_RUN_LOG_CHECKSUM_MISSING",
+ "owner": "community-moderator",
+ "message": "Reproduction run log should include a checksum for auditability."
+ },
+ {
+ "code": "COLLECT_REPRODUCER_DISCLOSURE",
+ "owner": "community-moderator",
+ "message": "Collect conflict and method disclosure before counting the run."
+ },
+ {
+ "code": "ADDRESS_INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD",
+ "owner": "community-moderator",
+ "message": "Not enough independent successful reproduction runs for the requested badge."
+ }
+ ],
+ "auditDigest": "0e2d116123f71255b2c9bb7cc54903f668742091be4e300579a88e13eea4f8ae"
+ }
+ ],
+ "audit": {
+ "source": "synthetic-reproducibility-badge-verification-gate",
+ "digest": "69aff17fd9e907aac7ecdc9f497360367824bc4ee0dd07a4d418e8f0b0f473f2"
+ }
+}
diff --git a/reproducibility-badge-verification-gate/reports/reproducibility-badge-report.md b/reproducibility-badge-verification-gate/reports/reproducibility-badge-report.md
new file mode 100644
index 00000000..7b6c38d0
--- /dev/null
+++ b/reproducibility-badge-verification-gate/reports/reproducibility-badge-report.md
@@ -0,0 +1,54 @@
+# Reproducibility Badge Verification Report
+
+Generated projects: 4
+Passed: 1
+Needs revision: 1
+Held: 2
+Valid independent runs: 3
+Audit digest: `69aff17fd9e907aac7ecdc9f497360367824bc4ee0dd07a4d418e8f0b0f473f2`
+
+## Badge Decisions
+
+### RB-PASS-001: Open enzyme model reproducibility packet
+- Status: pass
+- Requested badge: silver
+- Recommended badge: silver
+- Required artifacts: 3
+- Reproduction runs: 2
+- Valid independent runs: 2
+- Unique valid institutions: 2
+- Findings: none
+- First action: Issue the requested silver reproducibility badge.
+
+### RB-REV-002: Ecology sampling reproducibility packet
+- Status: revise
+- Requested badge: silver
+- Recommended badge: bronze
+- Required artifacts: 2
+- Reproduction runs: 1
+- Valid independent runs: 1
+- Unique valid institutions: 1
+- Findings: INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD
+- First action: Requested silver badge is not fully supported; consider bronze or collect more evidence.
+
+### RB-HOLD-003: Conflicted review badge packet
+- Status: hold
+- Requested badge: bronze
+- Recommended badge: none
+- Required artifacts: 2
+- Reproduction runs: 2
+- Valid independent runs: 0
+- Unique valid institutions: 0
+- Findings: REPRODUCER_AUTHOR_CONFLICT, SAME_INSTITUTION_REVIEW, RECENT_COLLABORATOR_CONFLICT, FUNDER_RELATIONSHIP_DISCLOSURE, INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD
+- First action: Request a new independent reproducer before issuing reputation credit.
+
+### RB-HOLD-004: Artifact mismatch badge packet
+- Status: hold
+- Requested badge: bronze
+- Recommended badge: none
+- Required artifacts: 3
+- Reproduction runs: 1
+- Valid independent runs: 0
+- Unique valid institutions: 0
+- Findings: ARTIFACT_CHECKSUM_MISSING, ARTIFACT_VERSION_MISSING, ENVIRONMENT_DIGEST_MISSING, LOCKFILE_CHECKSUM_MISSING, ARTIFACT_COVERAGE_INCOMPLETE, ENVIRONMENT_DIGEST_MISMATCH, OUTPUT_DIGEST_MISMATCH, REPRODUCTION_TESTS_FAILED, RUN_LOG_CHECKSUM_MISSING, REPRODUCER_DISCLOSURE_MISSING, INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD
+- First action: Complete artifact checksums and coverage before badge review.
diff --git a/reproducibility-badge-verification-gate/reports/summary.svg b/reproducibility-badge-verification-gate/reports/summary.svg
new file mode 100644
index 00000000..6b72bfe2
--- /dev/null
+++ b/reproducibility-badge-verification-gate/reports/summary.svg
@@ -0,0 +1,16 @@
+
diff --git a/reproducibility-badge-verification-gate/requirements-map.md b/reproducibility-badge-verification-gate/requirements-map.md
new file mode 100644
index 00000000..d0d6bccc
--- /dev/null
+++ b/reproducibility-badge-verification-gate/requirements-map.md
@@ -0,0 +1,14 @@
+# Requirements Map
+
+Issue #15 requirement | Coverage in this module
+--- | ---
+Peer validation and structured reviews | Models independent reproduction runs with role, conflict disclosure, artifact checks, output checks, and run-log evidence.
+Contributor credits and timestamps | Tracks badge request review time and completed reproduction runs as auditable evidence for reputation credit.
+Reputation scoring and reproducibility score | Produces deterministic bronze, silver, gold, or hold decisions from valid independent reproduction evidence.
+Transparent metrics | Emits findings, reviewer actions, valid independent run counts, institution diversity, and deterministic audit digests.
+Trust and quality incentives | Blocks conflicted or failed reproduction runs from earning reputation credit and recommends lower badges when evidence supports less than requested.
+Reviewer-ready evidence | Generates JSON, Markdown, SVG, and MP4 artifacts from synthetic data only.
+
+## Non-overlap Notes
+
+This PR avoids duplicating existing SCIBASE issue #15 slices for broad reputation ledgers, appeal guardrails, review calibration, COI assignment, endorsement rings, badge renewal, leaderboards, civility checks, timeliness credits, mentorship ladders, anonymous identity leak checks, and transparency receipts. The module focuses on first-time reproducibility badge verification.
diff --git a/reproducibility-badge-verification-gate/sample-data.js b/reproducibility-badge-verification-gate/sample-data.js
new file mode 100644
index 00000000..d5433c57
--- /dev/null
+++ b/reproducibility-badge-verification-gate/sample-data.js
@@ -0,0 +1,222 @@
+const badgePolicy = {
+ badgeLevels: ["bronze", "silver", "gold"],
+ minIndependentRunsByBadge: {
+ bronze: 1,
+ silver: 2,
+ gold: 3,
+ },
+ allowedReproducerRoles: ["independent-reproducer", "community-reviewer", "external-methods-reviewer"],
+ minArtifactCoverage: 1,
+ minInstitutionsForMultiRunBadge: 2,
+ maxRunAgeDays: 90,
+ allowLowerBadgeRecommendation: true,
+}
+
+const projects = [
+ {
+ id: "RB-PASS-001",
+ title: "Open enzyme model reproducibility packet",
+ authors: [
+ { id: "author-a", institution: "North Lab" },
+ { id: "author-b", institution: "North Lab" },
+ ],
+ badgeRequest: {
+ level: "silver",
+ reviewedAt: "2026-05-21T18:35:00.000Z",
+ },
+ artifacts: [
+ { id: "dataset", type: "dataset", checksum: "sha256:dataset-pass", version: "v1.0.0", required: true },
+ { id: "analysis", type: "notebook", checksum: "sha256:notebook-pass", version: "v1.0.0", required: true },
+ { id: "environment", type: "lockfile", checksum: "sha256:lock-pass", version: "v1.0.0", required: true },
+ ],
+ environments: [
+ { id: "container", runtime: "python-3.12", digest: "sha256:env-pass", lockfileChecksum: "sha256:lock-pass" },
+ ],
+ reproductionRuns: [
+ {
+ id: "run-a",
+ reproducerId: "rep-101",
+ role: "independent-reproducer",
+ institution: "West Methods Institute",
+ relatedAuthorIds: [],
+ recentCollaboratorAuthorIds: [],
+ fundingRelationship: false,
+ artifactChecks: [
+ { artifactId: "dataset", checksumMatches: true },
+ { artifactId: "analysis", checksumMatches: true },
+ { artifactId: "environment", checksumMatches: true },
+ ],
+ environmentDigestMatches: true,
+ outputDigestMatches: true,
+ testsPassed: true,
+ runLogChecksum: "sha256:run-a-log",
+ disclosureSigned: true,
+ completedAt: "2026-05-18T09:00:00.000Z",
+ },
+ {
+ id: "run-b",
+ reproducerId: "rep-202",
+ role: "external-methods-reviewer",
+ institution: "South Reproducibility Center",
+ relatedAuthorIds: [],
+ recentCollaboratorAuthorIds: [],
+ fundingRelationship: false,
+ artifactChecks: [
+ { artifactId: "dataset", checksumMatches: true },
+ { artifactId: "analysis", checksumMatches: true },
+ { artifactId: "environment", checksumMatches: true },
+ ],
+ environmentDigestMatches: true,
+ outputDigestMatches: true,
+ testsPassed: true,
+ runLogChecksum: "sha256:run-b-log",
+ disclosureSigned: true,
+ completedAt: "2026-05-19T11:30:00.000Z",
+ },
+ ],
+ },
+ {
+ id: "RB-REV-002",
+ title: "Ecology sampling reproducibility packet",
+ authors: [
+ { id: "author-c", institution: "Coastal University" },
+ ],
+ badgeRequest: {
+ level: "silver",
+ reviewedAt: "2026-05-21T18:35:00.000Z",
+ },
+ artifacts: [
+ { id: "dataset", type: "dataset", checksum: "sha256:eco-dataset", version: "v1.1.0", required: true },
+ { id: "script", type: "script", checksum: "sha256:eco-script", version: "v1.1.0", required: true },
+ ],
+ environments: [
+ { id: "conda", runtime: "r-4.4", digest: "sha256:eco-env", lockfileChecksum: "sha256:eco-lock" },
+ ],
+ reproductionRuns: [
+ {
+ id: "run-c",
+ reproducerId: "rep-303",
+ role: "independent-reproducer",
+ institution: "Mountain Stats Lab",
+ relatedAuthorIds: [],
+ recentCollaboratorAuthorIds: [],
+ fundingRelationship: false,
+ artifactChecks: [
+ { artifactId: "dataset", checksumMatches: true },
+ { artifactId: "script", checksumMatches: true },
+ ],
+ environmentDigestMatches: true,
+ outputDigestMatches: true,
+ testsPassed: true,
+ runLogChecksum: "sha256:run-c-log",
+ disclosureSigned: true,
+ completedAt: "2026-05-20T08:00:00.000Z",
+ },
+ ],
+ },
+ {
+ id: "RB-HOLD-003",
+ title: "Conflicted review badge packet",
+ authors: [
+ { id: "author-d", institution: "North Lab" },
+ { id: "author-e", institution: "North Lab" },
+ ],
+ badgeRequest: {
+ level: "bronze",
+ reviewedAt: "2026-05-21T18:35:00.000Z",
+ },
+ artifacts: [
+ { id: "dataset", type: "dataset", checksum: "sha256:conflict-dataset", version: "v0.9.0", required: true },
+ { id: "notebook", type: "notebook", checksum: "sha256:conflict-notebook", version: "v0.9.0", required: true },
+ ],
+ environments: [
+ { id: "container", runtime: "python-3.11", digest: "sha256:conflict-env", lockfileChecksum: "sha256:conflict-lock" },
+ ],
+ reproductionRuns: [
+ {
+ id: "run-d",
+ reproducerId: "author-d",
+ role: "community-reviewer",
+ institution: "North Lab",
+ relatedAuthorIds: ["author-d"],
+ recentCollaboratorAuthorIds: [],
+ fundingRelationship: false,
+ artifactChecks: [
+ { artifactId: "dataset", checksumMatches: true },
+ { artifactId: "notebook", checksumMatches: true },
+ ],
+ environmentDigestMatches: true,
+ outputDigestMatches: true,
+ testsPassed: true,
+ runLogChecksum: "sha256:run-d-log",
+ disclosureSigned: true,
+ completedAt: "2026-05-20T10:00:00.000Z",
+ },
+ {
+ id: "run-e",
+ reproducerId: "rep-404",
+ role: "independent-reproducer",
+ institution: "East Review Lab",
+ relatedAuthorIds: [],
+ recentCollaboratorAuthorIds: ["author-e"],
+ fundingRelationship: true,
+ artifactChecks: [
+ { artifactId: "dataset", checksumMatches: true },
+ { artifactId: "notebook", checksumMatches: true },
+ ],
+ environmentDigestMatches: true,
+ outputDigestMatches: true,
+ testsPassed: true,
+ runLogChecksum: "sha256:run-e-log",
+ disclosureSigned: true,
+ completedAt: "2026-05-19T10:00:00.000Z",
+ },
+ ],
+ },
+ {
+ id: "RB-HOLD-004",
+ title: "Artifact mismatch badge packet",
+ authors: [
+ { id: "author-f", institution: "Signal Bio Lab" },
+ ],
+ badgeRequest: {
+ level: "bronze",
+ reviewedAt: "2026-05-21T18:35:00.000Z",
+ },
+ artifacts: [
+ { id: "dataset", type: "dataset", checksum: "", version: "v2.0.0", required: true },
+ { id: "analysis", type: "notebook", checksum: "sha256:analysis-hold", version: "v2.0.0", required: true },
+ { id: "results", type: "figure", checksum: "sha256:results-hold", version: "", required: true },
+ ],
+ environments: [
+ { id: "container", runtime: "julia-1.11", digest: "", lockfileChecksum: "" },
+ ],
+ reproductionRuns: [
+ {
+ id: "run-f",
+ reproducerId: "rep-505",
+ role: "independent-reproducer",
+ institution: "Open Repro Guild",
+ relatedAuthorIds: [],
+ recentCollaboratorAuthorIds: [],
+ fundingRelationship: false,
+ artifactChecks: [
+ { artifactId: "dataset", checksumMatches: false },
+ { artifactId: "analysis", checksumMatches: true },
+ { artifactId: "results", checksumMatches: false },
+ ],
+ environmentDigestMatches: false,
+ outputDigestMatches: false,
+ testsPassed: false,
+ runLogChecksum: "",
+ disclosureSigned: false,
+ completedAt: "2026-04-01T10:00:00.000Z",
+ },
+ ],
+ },
+]
+
+module.exports = {
+ badgePolicy,
+ projects,
+}
diff --git a/reproducibility-badge-verification-gate/test.js b/reproducibility-badge-verification-gate/test.js
new file mode 100644
index 00000000..e1aeb29c
--- /dev/null
+++ b/reproducibility-badge-verification-gate/test.js
@@ -0,0 +1,45 @@
+const assert = require("node:assert/strict")
+const { evaluateBadgePortfolio, evaluateProjectBadge } = require("./index")
+const { badgePolicy, projects } = require("./sample-data")
+
+const packet = evaluateBadgePortfolio({ projects, policy: badgePolicy })
+
+assert.equal(packet.summary.totalProjects, 4)
+assert.equal(packet.summary.passed, 1)
+assert.equal(packet.summary.revise, 1)
+assert.equal(packet.summary.held, 2)
+assert.equal(packet.summary.validIndependentRuns, 3)
+assert.equal(packet.summary.requestedBadges.silver, 2)
+assert.equal(packet.summary.requestedBadges.bronze, 2)
+assert.match(packet.audit.digest, /^[a-f0-9]{64}$/)
+
+const pass = packet.decisions.find((decision) => decision.id === "RB-PASS-001")
+assert.equal(pass.status, "pass")
+assert.equal(pass.recommendedBadge, "silver")
+assert.equal(pass.evidence.validIndependentRuns, 2)
+assert.equal(pass.findings.length, 0)
+assert.equal(pass.reviewerActions[0].code, "ISSUE_REPRODUCIBILITY_BADGE")
+
+const revise = packet.decisions.find((decision) => decision.id === "RB-REV-002")
+assert.equal(revise.status, "revise")
+assert.equal(revise.requestedBadge, "silver")
+assert.equal(revise.recommendedBadge, "bronze")
+assert.ok(revise.findings.some((finding) => finding.code === "INDEPENDENT_RUNS_BELOW_BADGE_THRESHOLD"))
+assert.equal(revise.reviewerActions[0].code, "ISSUE_LOWER_BADGE_OR_COLLECT_EVIDENCE")
+
+const conflict = evaluateProjectBadge(projects.find((project) => project.id === "RB-HOLD-003"), badgePolicy)
+assert.equal(conflict.status, "hold")
+assert.equal(conflict.evidence.validIndependentRuns, 0)
+assert.ok(conflict.findings.some((finding) => finding.code === "REPRODUCER_AUTHOR_CONFLICT"))
+assert.ok(conflict.findings.some((finding) => finding.code === "RECENT_COLLABORATOR_CONFLICT"))
+assert.ok(conflict.reviewerActions.some((action) => action.code === "REPLACE_CONFLICTED_REPRODUCER"))
+
+const mismatch = packet.decisions.find((decision) => decision.id === "RB-HOLD-004")
+assert.equal(mismatch.status, "hold")
+assert.equal(mismatch.recommendedBadge, "none")
+assert.ok(mismatch.findings.some((finding) => finding.code === "ARTIFACT_CHECKSUM_MISSING"))
+assert.ok(mismatch.findings.some((finding) => finding.code === "ENVIRONMENT_DIGEST_MISSING"))
+assert.ok(mismatch.findings.some((finding) => finding.code === "REPRODUCTION_TESTS_FAILED"))
+assert.ok(mismatch.findings.some((finding) => finding.code === "REPRODUCER_DISCLOSURE_MISSING"))
+
+console.log("reproducibility-badge-verification-gate tests passed")