diff --git a/mentorship-impact-ladder/README.md b/mentorship-impact-ladder/README.md new file mode 100644 index 0000000..2c1dfd2 --- /dev/null +++ b/mentorship-impact-ladder/README.md @@ -0,0 +1,28 @@ +# Mentorship Impact Ladder + +This module is a focused slice for SCIBASE.AI issue #15, Community & User Reputation System. + +It evaluates mentor/trainee reputation progression using supervised peer reviews, reproducibility reruns, dataset/code contributions, CRediT roles, independent endorsements, and conflict safeguards. The sample data is synthetic and dependency-free. + +## What It Covers + +- Mentorship-specific badge tiers such as emerging mentor, trusted mentor, and open science champion. +- Trainee growth credit from supervised reviews, validation work, dataset curation, and project comments. +- Mentor reputation deltas that require independent endorsements and reproducibility-linked evidence. +- Conflict and nepotism safeguards before badge or leaderboard promotion. +- Institution-ready mentorship packets with deterministic digests. +- JSON audit packets, Markdown review packets, SVG summaries, and a short MP4 demo artifact. + +## Run + +```bash +node mentorship-impact-ladder/test.js +node mentorship-impact-ladder/demo.js +``` + +Demo output is written to: + +- `reports/mentorship-audit.json` +- `reports/institution-packet.md` +- `reports/mentorship-ladder.svg` +- `reports/demo.mp4` diff --git a/mentorship-impact-ladder/demo.js b/mentorship-impact-ladder/demo.js new file mode 100644 index 0000000..420bbdf --- /dev/null +++ b/mentorship-impact-ladder/demo.js @@ -0,0 +1,25 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const data = require("./sample-data"); +const { renderMarkdown, renderSvg, runMentorshipAudit } = require("./index"); + +const report = runMentorshipAudit(data); +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); +fs.writeFileSync(path.join(reportsDir, "mentorship-audit.json"), JSON.stringify(report, null, 2)); +fs.writeFileSync(path.join(reportsDir, "institution-packet.md"), renderMarkdown(report)); +fs.writeFileSync(path.join(reportsDir, "mentorship-ladder.svg"), renderSvg(report)); + +console.log(JSON.stringify({ + averageScore: report.summary.averageScore, + badgeEligibleCount: report.summary.badgeEligibleCount, + moderatorReviewCount: report.summary.moderatorReviewCount, + totalFindings: report.summary.totalFindings, + auditDigest: report.auditDigest, + reports: [ + "reports/mentorship-audit.json", + "reports/institution-packet.md", + "reports/mentorship-ladder.svg", + "reports/demo.mp4" + ] +}, null, 2)); diff --git a/mentorship-impact-ladder/index.js b/mentorship-impact-ladder/index.js new file mode 100644 index 0000000..c9b309a --- /dev/null +++ b/mentorship-impact-ladder/index.js @@ -0,0 +1,265 @@ +const crypto = require("node:crypto"); + +const ROLE_WEIGHTS = { + review: 8, + methodology: 9, + software: 9, + validation: 10, + "data-curation": 9, + supervision: 7 +}; + +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return crypto.createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function byId(items) { + return new Map(items.map((item) => [item.id, item])); +} + +function mean(values) { + if (!values.length) return 0; + return values.reduce((sum, value) => sum + value, 0) / values.length; +} + +function roleCoverageScore(evidenceItems) { + const roles = new Set(evidenceItems.flatMap((item) => item.creditedRoles || [])); + const score = [...roles].reduce((total, role) => total + (ROLE_WEIGHTS[role] || 4), 0); + return Math.min(28, score); +} + +function endorsementScore(evidenceItems) { + return Math.min(18, evidenceItems.filter((item) => item.independentEndorsement).length * 6); +} + +function reproducibilityScore(evidenceItems) { + return Math.min(18, evidenceItems.filter((item) => item.reproducibilityLinked).length * 6); +} + +function visibilityScore(evidenceItems) { + const publicCount = evidenceItems.filter((item) => item.publicMode === "public").length; + const semiPrivateCount = evidenceItems.filter((item) => item.publicMode === "semi-private").length; + return Math.min(12, publicCount * 4 + semiPrivateCount * 2); +} + +function evidenceQualityScore(evidenceItems) { + return Math.round(mean(evidenceItems.map((item) => item.qualityScore)) * 0.24); +} + +function detectSafeguards(pair, mentor, trainee, evidenceItems) { + const findings = []; + if (!pair.acknowledgements.includes(pair.traineeId)) { + findings.push({ + severity: "high", + code: "trainee-acknowledgement-missing", + message: `${trainee.displayName} has not acknowledged mentorship credit for ${pair.id}.`, + task: "Hold badge promotion until the trainee acknowledges the mentorship packet." + }); + } + if (mentor.conflicts.includes(pair.traineeId) || trainee.conflicts.includes(pair.mentorId) || pair.declaredRelationship === "family-member") { + findings.push({ + severity: "high", + code: "relationship-conflict-review-required", + message: `${pair.id} has a declared relationship conflict that can bias reputation credit.`, + task: "Route this mentorship packet to an independent moderator before assigning badge credit." + }); + } + if (!evidenceItems.some((item) => item.independentEndorsement)) { + findings.push({ + severity: "medium", + code: "independent-endorsement-missing", + message: `${pair.id} has no independent endorsement evidence.`, + task: "Request an independent project owner or reviewer endorsement." + }); + } + if (!evidenceItems.some((item) => item.reproducibilityLinked)) { + findings.push({ + severity: "medium", + code: "reproducibility-evidence-missing", + message: `${pair.id} lacks reproducibility-linked mentorship evidence.`, + task: "Attach a rerun, validation, or artifact verification record." + }); + } + if (new Set(evidenceItems.map((item) => item.projectId)).size < 2 && evidenceItems.length < 4) { + findings.push({ + severity: "low", + code: "portfolio-breadth-low", + message: `${pair.id} has limited project breadth for a durable mentorship badge.`, + task: "Collect one more supervised contribution on a separate project." + }); + } + return findings; +} + +function tierFor(score, findings, ladder) { + if (findings.some((finding) => finding.code === "relationship-conflict-review-required")) return "moderator-review"; + if (score >= ladder.thresholds.champion) return "open-science-champion"; + if (score >= ladder.thresholds.trusted) return "trusted-mentor"; + if (score >= ladder.thresholds.emerging) return "emerging-mentor"; + return "not-yet-eligible"; +} + +function evaluatePair(pair, context) { + const mentor = context.members.get(pair.mentorId); + const trainee = context.members.get(pair.traineeId); + const evidenceItems = pair.evidenceIds.map((id) => context.evidence.get(id)).filter(Boolean); + const findings = detectSafeguards(pair, mentor, trainee, evidenceItems); + const score = Math.min(100, Math.round( + evidenceQualityScore(evidenceItems) + + roleCoverageScore(evidenceItems) + + endorsementScore(evidenceItems) + + reproducibilityScore(evidenceItems) + + visibilityScore(evidenceItems) + + mentor.reputation * 0.08 + )); + const tier = tierFor(score, findings, context.ladder); + const traineeGrowth = Math.max(0, Math.round(score * 0.18 - findings.length * 2)); + const mentorDelta = tier === "moderator-review" ? 0 : Math.round(score * 0.1); + return { + mentorshipId: pair.id, + mentor: { id: mentor.id, displayName: mentor.displayName }, + trainee: { id: trainee.id, displayName: trainee.displayName }, + score, + tier, + traineeGrowth, + mentorDelta, + evidenceCount: evidenceItems.length, + creditedRoles: [...new Set(evidenceItems.flatMap((item) => item.creditedRoles || []))].sort(), + findings, + packetDigest: digest({ + pair, + evidenceItems, + score, + tier, + findings + }) + }; +} + +function summarize(pairReports) { + const allFindings = pairReports.flatMap((report) => report.findings); + const countsByTier = pairReports.reduce((counts, report) => { + counts[report.tier] = (counts[report.tier] || 0) + 1; + return counts; + }, {}); + const countsBySeverity = allFindings.reduce((counts, finding) => { + counts[finding.severity] = (counts[finding.severity] || 0) + 1; + return counts; + }, {}); + return { + averageScore: Math.round(mean(pairReports.map((report) => report.score))), + badgeEligibleCount: pairReports.filter((report) => ["open-science-champion", "trusted-mentor", "emerging-mentor"].includes(report.tier)).length, + moderatorReviewCount: pairReports.filter((report) => report.tier === "moderator-review").length, + totalFindings: allFindings.length, + countsByTier, + countsBySeverity + }; +} + +function buildInstitutionPacket(pairReports) { + return pairReports.map((report) => ({ + mentorshipId: report.mentorshipId, + mentorId: report.mentor.id, + traineeId: report.trainee.id, + tier: report.tier, + creditedRoles: report.creditedRoles, + traineeGrowth: report.traineeGrowth, + mentorDelta: report.mentorDelta, + packetDigest: report.packetDigest, + actions: report.findings.map((finding) => finding.task) + })); +} + +function runMentorshipAudit(data) { + const context = { + members: byId(data.members), + evidence: byId(data.evidence), + ladder: data.community.ladders[0] + }; + const pairReports = data.mentorships.map((pair) => evaluatePair(pair, context)); + const packet = { + community: { + id: data.community.id, + domain: data.community.domain, + reviewPeriod: data.community.reviewPeriod + }, + module: "mentorship-impact-ladder", + summary: summarize(pairReports), + pairReports, + institutionPacket: buildInstitutionPacket(pairReports) + }; + return { + ...packet, + auditDigest: digest(packet) + }; +} + +function renderMarkdown(report) { + const lines = [ + `# Mentorship Impact Ladder: ${report.community.domain}`, + "", + `Average score: **${report.summary.averageScore}**`, + `Badge eligible pairs: **${report.summary.badgeEligibleCount}**`, + `Moderator review pairs: **${report.summary.moderatorReviewCount}**`, + "", + "## Mentorship Packets" + ]; + for (const pair of report.pairReports) { + lines.push("", `### ${pair.mentorshipId}: ${pair.mentor.displayName} -> ${pair.trainee.displayName}`); + lines.push(`Tier: **${pair.tier}**`); + lines.push(`Score: **${pair.score}**`); + lines.push(`Roles: ${pair.creditedRoles.join(", ") || "none"}`); + if (!pair.findings.length) { + lines.push("- No safeguards blocking badge progression."); + } else { + for (const finding of pair.findings) { + lines.push(`- [${finding.severity}] ${finding.message}`); + lines.push(` Task: ${finding.task}`); + } + } + } + lines.push("", `Audit digest: \`${report.auditDigest}\``); + return `${lines.join("\n")}\n`; +} + +function renderSvg(report) { + const width = 960; + const rowHeight = 92; + const height = 158 + report.pairReports.length * rowHeight; + const rows = report.pairReports.map((pair, index) => { + const y = 142 + index * rowHeight; + const color = pair.tier === "moderator-review" ? "#c53030" : pair.score >= 88 ? "#2f855a" : pair.score >= 70 ? "#2b6cb0" : "#b7791f"; + return [ + `${pair.mentorshipId}`, + ``, + ``, + `${pair.tier} (${pair.score})`, + `${pair.mentor.displayName} -> ${pair.trainee.displayName} | ${pair.findings.length} finding(s)` + ].join(""); + }).join(""); + return [ + ``, + ``, + ``, + `Mentorship Impact Ladder`, + `Eligible ${report.summary.badgeEligibleCount} | Moderator review ${report.summary.moderatorReviewCount} | Audit ${report.auditDigest.slice(0, 12)}`, + rows, + `` + ].join(""); +} + +module.exports = { + runMentorshipAudit, + evaluatePair, + renderMarkdown, + renderSvg, + digest +}; diff --git a/mentorship-impact-ladder/reports/demo.mp4 b/mentorship-impact-ladder/reports/demo.mp4 new file mode 100644 index 0000000..cd3e192 Binary files /dev/null and b/mentorship-impact-ladder/reports/demo.mp4 differ diff --git a/mentorship-impact-ladder/reports/institution-packet.md b/mentorship-impact-ladder/reports/institution-packet.md new file mode 100644 index 0000000..622128d --- /dev/null +++ b/mentorship-impact-ladder/reports/institution-packet.md @@ -0,0 +1,39 @@ +# Mentorship Impact Ladder: computational-neuroscience + +Average score: **65** +Badge eligible pairs: **2** +Moderator review pairs: **1** + +## Mentorship Packets + +### pair-1: Dr. Mira Chen -> Lina Gomez +Tier: **open-science-champion** +Score: **100** +Roles: data-curation, methodology, review, software, supervision, validation +- No safeguards blocking badge progression. + +### pair-2: Dr. Omar Patel -> Kai Smith +Tier: **emerging-mentor** +Score: **52** +Roles: review +- [medium] pair-2 lacks reproducibility-linked mentorship evidence. + Task: Attach a rerun, validation, or artifact verification record. +- [low] pair-2 has limited project breadth for a durable mentorship badge. + Task: Collect one more supervised contribution on a separate project. + +### pair-3: Dr. Mira Chen -> Noah Chen +Tier: **moderator-review** +Score: **43** +Roles: review, supervision +- [high] Noah Chen has not acknowledged mentorship credit for pair-3. + Task: Hold badge promotion until the trainee acknowledges the mentorship packet. +- [high] pair-3 has a declared relationship conflict that can bias reputation credit. + Task: Route this mentorship packet to an independent moderator before assigning badge credit. +- [medium] pair-3 has no independent endorsement evidence. + Task: Request an independent project owner or reviewer endorsement. +- [medium] pair-3 lacks reproducibility-linked mentorship evidence. + Task: Attach a rerun, validation, or artifact verification record. +- [low] pair-3 has limited project breadth for a durable mentorship badge. + Task: Collect one more supervised contribution on a separate project. + +Audit digest: `5e5b5aeb170d943b0ed87df8174939e9c115c5c71fffb203f341f24cfc5a3d91` diff --git a/mentorship-impact-ladder/reports/mentorship-audit.json b/mentorship-impact-ladder/reports/mentorship-audit.json new file mode 100644 index 0000000..b4253d4 --- /dev/null +++ b/mentorship-impact-ladder/reports/mentorship-audit.json @@ -0,0 +1,196 @@ +{ + "community": { + "id": "community-neuro-tools", + "domain": "computational-neuroscience", + "reviewPeriod": "2026-Q2" + }, + "module": "mentorship-impact-ladder", + "summary": { + "averageScore": 65, + "badgeEligibleCount": 2, + "moderatorReviewCount": 1, + "totalFindings": 7, + "countsByTier": { + "open-science-champion": 1, + "emerging-mentor": 1, + "moderator-review": 1 + }, + "countsBySeverity": { + "medium": 3, + "low": 2, + "high": 2 + } + }, + "pairReports": [ + { + "mentorshipId": "pair-1", + "mentor": { + "id": "mentor-a", + "displayName": "Dr. Mira Chen" + }, + "trainee": { + "id": "trainee-a", + "displayName": "Lina Gomez" + }, + "score": 100, + "tier": "open-science-champion", + "traineeGrowth": 18, + "mentorDelta": 10, + "evidenceCount": 5, + "creditedRoles": [ + "data-curation", + "methodology", + "review", + "software", + "supervision", + "validation" + ], + "findings": [], + "packetDigest": "5ca41608d183976afc9e1e4a782bcb228ac8560dae5f1c6356aba9abe498ecd5" + }, + { + "mentorshipId": "pair-2", + "mentor": { + "id": "mentor-b", + "displayName": "Dr. Omar Patel" + }, + "trainee": { + "id": "trainee-b", + "displayName": "Kai Smith" + }, + "score": 52, + "tier": "emerging-mentor", + "traineeGrowth": 5, + "mentorDelta": 5, + "evidenceCount": 3, + "creditedRoles": [ + "review" + ], + "findings": [ + { + "severity": "medium", + "code": "reproducibility-evidence-missing", + "message": "pair-2 lacks reproducibility-linked mentorship evidence.", + "task": "Attach a rerun, validation, or artifact verification record." + }, + { + "severity": "low", + "code": "portfolio-breadth-low", + "message": "pair-2 has limited project breadth for a durable mentorship badge.", + "task": "Collect one more supervised contribution on a separate project." + } + ], + "packetDigest": "0bfd6ac0d0f517b0495891b1c56b555c147ffb729d1cb9e6da910b9e85e28768" + }, + { + "mentorshipId": "pair-3", + "mentor": { + "id": "mentor-a", + "displayName": "Dr. Mira Chen" + }, + "trainee": { + "id": "trainee-c", + "displayName": "Noah Chen" + }, + "score": 43, + "tier": "moderator-review", + "traineeGrowth": 0, + "mentorDelta": 0, + "evidenceCount": 2, + "creditedRoles": [ + "review", + "supervision" + ], + "findings": [ + { + "severity": "high", + "code": "trainee-acknowledgement-missing", + "message": "Noah Chen has not acknowledged mentorship credit for pair-3.", + "task": "Hold badge promotion until the trainee acknowledges the mentorship packet." + }, + { + "severity": "high", + "code": "relationship-conflict-review-required", + "message": "pair-3 has a declared relationship conflict that can bias reputation credit.", + "task": "Route this mentorship packet to an independent moderator before assigning badge credit." + }, + { + "severity": "medium", + "code": "independent-endorsement-missing", + "message": "pair-3 has no independent endorsement evidence.", + "task": "Request an independent project owner or reviewer endorsement." + }, + { + "severity": "medium", + "code": "reproducibility-evidence-missing", + "message": "pair-3 lacks reproducibility-linked mentorship evidence.", + "task": "Attach a rerun, validation, or artifact verification record." + }, + { + "severity": "low", + "code": "portfolio-breadth-low", + "message": "pair-3 has limited project breadth for a durable mentorship badge.", + "task": "Collect one more supervised contribution on a separate project." + } + ], + "packetDigest": "fba97b73e812afea2878a2284926d6c8bfeefe9aac3a88bee16a858d384ede97" + } + ], + "institutionPacket": [ + { + "mentorshipId": "pair-1", + "mentorId": "mentor-a", + "traineeId": "trainee-a", + "tier": "open-science-champion", + "creditedRoles": [ + "data-curation", + "methodology", + "review", + "software", + "supervision", + "validation" + ], + "traineeGrowth": 18, + "mentorDelta": 10, + "packetDigest": "5ca41608d183976afc9e1e4a782bcb228ac8560dae5f1c6356aba9abe498ecd5", + "actions": [] + }, + { + "mentorshipId": "pair-2", + "mentorId": "mentor-b", + "traineeId": "trainee-b", + "tier": "emerging-mentor", + "creditedRoles": [ + "review" + ], + "traineeGrowth": 5, + "mentorDelta": 5, + "packetDigest": "0bfd6ac0d0f517b0495891b1c56b555c147ffb729d1cb9e6da910b9e85e28768", + "actions": [ + "Attach a rerun, validation, or artifact verification record.", + "Collect one more supervised contribution on a separate project." + ] + }, + { + "mentorshipId": "pair-3", + "mentorId": "mentor-a", + "traineeId": "trainee-c", + "tier": "moderator-review", + "creditedRoles": [ + "review", + "supervision" + ], + "traineeGrowth": 0, + "mentorDelta": 0, + "packetDigest": "fba97b73e812afea2878a2284926d6c8bfeefe9aac3a88bee16a858d384ede97", + "actions": [ + "Hold badge promotion until the trainee acknowledges the mentorship packet.", + "Route this mentorship packet to an independent moderator before assigning badge credit.", + "Request an independent project owner or reviewer endorsement.", + "Attach a rerun, validation, or artifact verification record.", + "Collect one more supervised contribution on a separate project." + ] + } + ], + "auditDigest": "5e5b5aeb170d943b0ed87df8174939e9c115c5c71fffb203f341f24cfc5a3d91" +} \ No newline at end of file diff --git a/mentorship-impact-ladder/reports/mentorship-ladder.svg b/mentorship-impact-ladder/reports/mentorship-ladder.svg new file mode 100644 index 0000000..a897379 --- /dev/null +++ b/mentorship-impact-ladder/reports/mentorship-ladder.svg @@ -0,0 +1 @@ +Mentorship Impact LadderEligible 2 | Moderator review 1 | Audit 5e5b5aeb170dpair-1open-science-champion (100)Dr. Mira Chen -> Lina Gomez | 0 finding(s)pair-2emerging-mentor (52)Dr. Omar Patel -> Kai Smith | 2 finding(s)pair-3moderator-review (43)Dr. Mira Chen -> Noah Chen | 5 finding(s) \ No newline at end of file diff --git a/mentorship-impact-ladder/requirements-map.md b/mentorship-impact-ladder/requirements-map.md new file mode 100644 index 0000000..018fedc --- /dev/null +++ b/mentorship-impact-ladder/requirements-map.md @@ -0,0 +1,17 @@ +# Requirements Map + +Source: SCIBASE.AI issue #15, Community & User Reputation System. + +| Requirement | Implementation | +| --- | --- | +| Peer reviews and comments | Supervised peer reviews and inline review comments are mentorship evidence inputs. | +| Review history on profiles | Pair reports emit mentor and trainee progression deltas for profile timelines. | +| Contributor credits | Evidence records carry CRediT-style roles such as review, validation, software, data-curation, and supervision. | +| Visible credit on researcher profiles | `institutionPacket` provides profile-safe mentorship credit summaries and digests. | +| CRediT taxonomy support | `creditedRoles` are normalized and scored through role weights. | +| Transparent reputation metrics | Scores are built from quality, role coverage, endorsements, reproducibility, visibility, and mentor baseline reputation. | +| Badges and incentive tiers | The ladder emits emerging mentor, trusted mentor, open science champion, and moderator-review tiers. | +| Abuse resistance | Relationship conflicts, missing trainee acknowledgement, missing endorsements, and missing reproducibility evidence create moderator or repair tasks. | +| Institutional reporting | Markdown packets and deterministic packet digests support promotion and mentorship reporting. | +| Reviewer-facing artifacts | Demo emits JSON, Markdown, SVG, and MP4 artifacts under `reports/`. | +| Local verification | `test.js` covers tier assignment, conflict safeguards, trainee acknowledgement, reproducibility requirements, digest stability, Markdown output, and SVG output. | diff --git a/mentorship-impact-ladder/sample-data.js b/mentorship-impact-ladder/sample-data.js new file mode 100644 index 0000000..5baf1c5 --- /dev/null +++ b/mentorship-impact-ladder/sample-data.js @@ -0,0 +1,207 @@ +const community = { + id: "community-neuro-tools", + domain: "computational-neuroscience", + reviewPeriod: "2026-Q2", + ladders: [ + { + id: "open-science-mentor", + name: "Open Science Mentor", + thresholds: { + emerging: 45, + trusted: 70, + champion: 88 + } + } + ] +}; + +const members = [ + { + id: "mentor-a", + displayName: "Dr. Mira Chen", + role: "mentor", + institution: "Northlake Lab", + reputation: 82, + conflicts: ["trainee-c"], + priorBadge: "Trusted Reviewer" + }, + { + id: "mentor-b", + displayName: "Dr. Omar Patel", + role: "mentor", + institution: "Cedar Institute", + reputation: 61, + conflicts: [], + priorBadge: null + }, + { + id: "trainee-a", + displayName: "Lina Gomez", + role: "trainee", + institution: "Northlake Lab", + reputation: 34, + conflicts: [], + priorBadge: null + }, + { + id: "trainee-b", + displayName: "Kai Smith", + role: "trainee", + institution: "Cedar Institute", + reputation: 41, + conflicts: [], + priorBadge: null + }, + { + id: "trainee-c", + displayName: "Noah Chen", + role: "trainee", + institution: "Northlake Lab", + reputation: 29, + conflicts: ["mentor-a"], + priorBadge: null + } +]; + +const mentorships = [ + { + id: "pair-1", + mentorId: "mentor-a", + traineeId: "trainee-a", + startDate: "2026-01-12", + declaredRelationship: "formal-lab-mentor", + goal: "Train reproducible notebook review and dataset curation", + evidenceIds: ["rev-1", "rerun-1", "dataset-1", "comment-1", "credit-1"], + acknowledgements: ["trainee-a", "mentor-a", "project-owner-7"] + }, + { + id: "pair-2", + mentorId: "mentor-b", + traineeId: "trainee-b", + startDate: "2026-02-18", + declaredRelationship: "community-mentor", + goal: "Build first peer-review portfolio", + evidenceIds: ["rev-2", "comment-2", "credit-2"], + acknowledgements: ["trainee-b", "mentor-b"] + }, + { + id: "pair-3", + mentorId: "mentor-a", + traineeId: "trainee-c", + startDate: "2026-03-04", + declaredRelationship: "family-member", + goal: "Accelerate challenge reputation tier", + evidenceIds: ["rev-3", "credit-3"], + acknowledgements: ["mentor-a"] + } +]; + +const evidence = [ + { + id: "rev-1", + type: "supervised-peer-review", + projectId: "proj-brain-atlas", + qualityScore: 91, + publicMode: "semi-private", + creditedRoles: ["review", "methodology"], + independentEndorsement: true, + reproducibilityLinked: true + }, + { + id: "rerun-1", + type: "reproducibility-rerun", + projectId: "proj-brain-atlas", + qualityScore: 88, + publicMode: "public", + creditedRoles: ["software", "validation"], + independentEndorsement: true, + reproducibilityLinked: true + }, + { + id: "dataset-1", + type: "dataset-curation", + projectId: "proj-brain-atlas", + qualityScore: 84, + publicMode: "public", + creditedRoles: ["data-curation"], + independentEndorsement: true, + reproducibilityLinked: true + }, + { + id: "comment-1", + type: "inline-review-comment", + projectId: "proj-brain-atlas", + qualityScore: 78, + publicMode: "semi-private", + creditedRoles: ["review"], + independentEndorsement: false, + reproducibilityLinked: false + }, + { + id: "credit-1", + type: "credit-approval", + projectId: "proj-brain-atlas", + qualityScore: 95, + publicMode: "public", + creditedRoles: ["supervision", "validation", "data-curation"], + independentEndorsement: true, + reproducibilityLinked: true + }, + { + id: "rev-2", + type: "supervised-peer-review", + projectId: "proj-microbiome", + qualityScore: 68, + publicMode: "semi-private", + creditedRoles: ["review"], + independentEndorsement: false, + reproducibilityLinked: false + }, + { + id: "comment-2", + type: "inline-review-comment", + projectId: "proj-microbiome", + qualityScore: 71, + publicMode: "public", + creditedRoles: ["review"], + independentEndorsement: true, + reproducibilityLinked: false + }, + { + id: "credit-2", + type: "credit-approval", + projectId: "proj-microbiome", + qualityScore: 74, + publicMode: "public", + creditedRoles: ["review"], + independentEndorsement: true, + reproducibilityLinked: false + }, + { + id: "rev-3", + type: "supervised-peer-review", + projectId: "proj-private-challenge", + qualityScore: 82, + publicMode: "anonymous", + creditedRoles: ["review"], + independentEndorsement: false, + reproducibilityLinked: false + }, + { + id: "credit-3", + type: "credit-approval", + projectId: "proj-private-challenge", + qualityScore: 90, + publicMode: "anonymous", + creditedRoles: ["supervision"], + independentEndorsement: false, + reproducibilityLinked: false + } +]; + +module.exports = { + community, + members, + mentorships, + evidence +}; diff --git a/mentorship-impact-ladder/test.js b/mentorship-impact-ladder/test.js new file mode 100644 index 0000000..581450b --- /dev/null +++ b/mentorship-impact-ladder/test.js @@ -0,0 +1,33 @@ +const assert = require("node:assert/strict"); +const data = require("./sample-data"); +const { digest, renderMarkdown, renderSvg, runMentorshipAudit } = require("./index"); + +const report = runMentorshipAudit(data); +const byPair = new Map(report.pairReports.map((pair) => [pair.mentorshipId, pair])); + +assert.equal(report.module, "mentorship-impact-ladder"); +assert.equal(report.pairReports.length, 3); +assert.match(report.auditDigest, /^[a-f0-9]{64}$/); + +assert.equal(byPair.get("pair-1").tier, "open-science-champion"); +assert.ok(byPair.get("pair-1").score >= 88); +assert.equal(byPair.get("pair-1").findings.length, 0); +assert.ok(byPair.get("pair-1").creditedRoles.includes("validation")); + +assert.equal(byPair.get("pair-2").tier, "emerging-mentor"); +assert.ok(byPair.get("pair-2").findings.some((finding) => finding.code === "reproducibility-evidence-missing")); + +assert.equal(byPair.get("pair-3").tier, "moderator-review"); +assert.ok(byPair.get("pair-3").findings.some((finding) => finding.code === "trainee-acknowledgement-missing")); +assert.ok(byPair.get("pair-3").findings.some((finding) => finding.code === "relationship-conflict-review-required")); +assert.equal(byPair.get("pair-3").mentorDelta, 0); + +assert.equal(report.summary.badgeEligibleCount, 2); +assert.equal(report.summary.moderatorReviewCount, 1); +assert.ok(report.institutionPacket.every((packet) => packet.packetDigest)); + +assert.equal(digest({ b: 2, a: 1 }), digest({ a: 1, b: 2 })); +assert.ok(renderMarkdown(report).includes("Mentorship Impact Ladder")); +assert.ok(renderSvg(report).startsWith("