diff --git a/profile-credit-consent-visibility-guard/README.md b/profile-credit-consent-visibility-guard/README.md
new file mode 100644
index 00000000..5e359214
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/README.md
@@ -0,0 +1,38 @@
+# Profile Credit Consent Visibility Guard
+
+This module adds a focused Community & User Reputation System guard for issue #15.
+It evaluates whether CRediT contribution records, peer-review receipts, and
+reputation deltas can safely appear on researcher profiles, citation pages,
+leaderboards, or institutional exports.
+
+The guard is intentionally narrow. It does not implement another broad
+reputation ledger, endorsement-ring detector, badge gate, leaderboard gate,
+review civility tool, recusal module, calibration bench, identity-leak-only
+checker, or correction-impact ledger.
+
+## What It Checks
+
+- contributor profile-credit consent is active and not expired or revoked
+- ORCID/profile identity is verified before public reputation deltas publish
+- CRediT/review roles are accepted and evidence receipts are immutable
+- anonymous or double-blind review credits remain redacted until release
+- embargoed artifacts stay citation-only until the embargo date passes
+- private or sensitive evidence is restricted to institutional export packets
+- missing evidence blocks public profile and leaderboard updates
+
+## Commands
+
+```bash
+npm run check
+npm test
+npm run demo
+```
+
+The demo writes deterministic reviewer artifacts to `reports/`:
+
+- `summary.json`
+- `reviewer-packet.md`
+- `summary.svg`
+
+The sample data is synthetic only. No private user, account, payout, profile, or
+payment information is included.
diff --git a/profile-credit-consent-visibility-guard/acceptance-notes.md b/profile-credit-consent-visibility-guard/acceptance-notes.md
new file mode 100644
index 00000000..423c2e8a
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/acceptance-notes.md
@@ -0,0 +1,23 @@
+# Acceptance Notes
+
+- Dependency-free CommonJS module.
+- Synthetic sample records only.
+- No live service calls, credentials, secrets, payment data, payout data, or
+ private profile data.
+- Deterministic `sha256` audit digest for reviewer-visible output.
+- Tests cover:
+ - publishable public profile/citation credits
+ - double-blind review redaction
+ - embargoed citation-only credits
+ - revoked consent holds
+ - institutional-only private evidence
+ - missing or mutable evidence holds
+ - deterministic digest generation
+
+Expected validation:
+
+```bash
+npm run check
+npm test
+npm run demo
+```
diff --git a/profile-credit-consent-visibility-guard/demo.js b/profile-credit-consent-visibility-guard/demo.js
new file mode 100644
index 00000000..e5724eba
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/demo.js
@@ -0,0 +1,31 @@
+const fs = require("node:fs");
+const path = require("node:path");
+const {
+ analyzeProfileCredits,
+ renderMarkdownReport,
+ renderSvgSummary,
+} = require("./index");
+const { sampleProfileCreditPackets } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const result = analyzeProfileCredits(sampleProfileCreditPackets, {
+ asOf: "2026-05-22T12:00:00.000Z",
+});
+
+fs.writeFileSync(
+ path.join(reportsDir, "summary.json"),
+ `${JSON.stringify(result, null, 2)}\n`
+);
+fs.writeFileSync(
+ path.join(reportsDir, "reviewer-packet.md"),
+ renderMarkdownReport(result)
+);
+fs.writeFileSync(
+ path.join(reportsDir, "summary.svg"),
+ renderSvgSummary(result)
+);
+
+console.log("profile credit consent visibility guard demo artifacts written");
+console.log(`audit digest: ${result.auditDigest}`);
diff --git a/profile-credit-consent-visibility-guard/index.js b/profile-credit-consent-visibility-guard/index.js
new file mode 100644
index 00000000..e9dfe885
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/index.js
@@ -0,0 +1,420 @@
+const crypto = require("node:crypto");
+
+const CREDIT_ROLES = new Set([
+ "Conceptualization",
+ "Data curation",
+ "Formal analysis",
+ "Funding acquisition",
+ "Investigation",
+ "Methodology",
+ "Project administration",
+ "Resources",
+ "Software",
+ "Supervision",
+ "Validation",
+ "Visualization",
+ "Writing - original draft",
+ "Writing - review & editing",
+ "Reviewing",
+]);
+
+function parseDate(value, field) {
+ if (!value) return null;
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ throw new Error(`invalid date for ${field}`);
+ }
+ return date;
+}
+
+function requireFields(value, fields, label) {
+ for (const field of fields) {
+ if (value[field] === undefined || value[field] === null || value[field] === "") {
+ throw new Error(`missing required ${label} field: ${field}`);
+ }
+ }
+}
+
+function hasEvidence(credit) {
+ return Array.isArray(credit.evidenceIds) && credit.evidenceIds.length > 0;
+}
+
+function evaluateCredit(profile, credit, options) {
+ requireFields(profile, ["researcherId", "displayName", "profileConsent"], "profile");
+ requireFields(
+ credit,
+ [
+ "id",
+ "projectId",
+ "creditType",
+ "creditRole",
+ "targetSurface",
+ "requestedVisibility",
+ "artifactVisibility",
+ "reputationDelta",
+ ],
+ "credit"
+ );
+
+ const asOf = parseDate(options.asOf, "asOf");
+ const consent = profile.profileConsent || {};
+ const embargoUntil = parseDate(credit.embargoUntil, `embargoUntil for ${credit.id}`);
+ const anonymousUntil = parseDate(
+ credit.anonymousUntil,
+ `anonymousUntil for ${credit.id}`
+ );
+ const consentExpiresAt = parseDate(
+ consent.expiresAt,
+ `profileConsent.expiresAt for ${profile.researcherId}`
+ );
+ const reasons = [];
+ const actions = [];
+ const visibleFields = [];
+
+ const base = {
+ id: credit.id,
+ researcherId: profile.researcherId,
+ displayName: profile.displayName,
+ projectId: credit.projectId,
+ creditType: credit.creditType,
+ creditRole: credit.creditRole,
+ requestedVisibility: credit.requestedVisibility,
+ targetSurface: credit.targetSurface,
+ artifactVisibility: credit.artifactVisibility,
+ reputationDelta: Number(credit.reputationDelta || 0),
+ publishableReputationDelta: 0,
+ visibleFields,
+ reasons,
+ actions,
+ };
+
+ if (!CREDIT_ROLES.has(credit.creditRole)) {
+ reasons.push("credit role is not mapped to an accepted CRediT/review role");
+ }
+
+ if (!hasEvidence(credit)) {
+ reasons.push("credit has no evidence receipt ids");
+ }
+
+ if (!credit.sourceReceiptImmutable) {
+ reasons.push("source receipt is not immutable");
+ }
+
+ if (consent.status !== "granted") {
+ return {
+ ...base,
+ status: "hold",
+ visibilityDecision: "do_not_publish",
+ reasons: [
+ consent.status === "revoked"
+ ? "profile credit consent has been revoked"
+ : "profile credit consent is not granted",
+ ],
+ actions: [
+ "Remove credit from public profile surfaces",
+ "Retain private audit receipt only",
+ "Ask contributor to refresh consent in account settings",
+ ],
+ };
+ }
+
+ if (consentExpiresAt && consentExpiresAt <= asOf) {
+ return {
+ ...base,
+ status: "hold",
+ visibilityDecision: "do_not_publish",
+ reasons: ["profile credit consent has expired"],
+ actions: [
+ "Hold profile publication",
+ "Request renewed profile credit consent",
+ ],
+ };
+ }
+
+ if (reasons.length > 0) {
+ return {
+ ...base,
+ status: "hold",
+ visibilityDecision: "evidence_review",
+ actions: [
+ "Block reputation delta from profile and leaderboard",
+ "Attach missing evidence before publication",
+ "Route credit receipt to steward review",
+ ],
+ };
+ }
+
+ if (anonymousUntil && anonymousUntil > asOf) {
+ visibleFields.push("anonymous_reviewer_label", "review_type", "project_id");
+ return {
+ ...base,
+ status: "redacted",
+ visibilityDecision: "anonymous_until_release",
+ publishableReputationDelta: 0,
+ reasons: [
+ "double-blind review remains inside anonymous visibility window",
+ ],
+ actions: [
+ "Publish only anonymized review receipt",
+ "Hold profile and leaderboard reputation delta until anonymity release",
+ `Re-evaluate after ${credit.anonymousUntil}`,
+ ],
+ };
+ }
+
+ if (embargoUntil && embargoUntil > asOf) {
+ visibleFields.push("credit_role", "project_id", "embargo_label");
+ return {
+ ...base,
+ status: "citation_only",
+ visibilityDecision: "embargoed_credit",
+ publishableReputationDelta: 0,
+ reasons: ["linked artifact is still under embargo"],
+ actions: [
+ "Show citation-page placeholder only",
+ "Suppress public profile delta until embargo expires",
+ `Re-evaluate after ${credit.embargoUntil}`,
+ ],
+ };
+ }
+
+ if (
+ credit.artifactVisibility === "private" ||
+ credit.sensitiveEvidence ||
+ credit.requestedVisibility === "institutional_only" ||
+ consent.visibility === "institutional_only"
+ ) {
+ visibleFields.push("credit_role", "project_id", "institutional_receipt_id");
+ return {
+ ...base,
+ status: "institutional_only",
+ visibilityDecision: "restricted_export_only",
+ publishableReputationDelta: 0,
+ reasons: ["credit evidence is restricted to institutional export"],
+ actions: [
+ "Exclude from public profile and citation page",
+ "Include in institutional report with restricted evidence label",
+ "Require steward approval before public conversion",
+ ],
+ };
+ }
+
+ if (credit.requestedVisibility === "citation_only" || consent.visibility === "citation_only") {
+ visibleFields.push("credit_role", "project_id", "citation_receipt_id");
+ return {
+ ...base,
+ status: "citation_only",
+ visibilityDecision: "citation_page_only",
+ publishableReputationDelta: 0,
+ reasons: ["contributor consent permits citation page but not public profile"],
+ actions: [
+ "Show contribution on citation page only",
+ "Suppress profile badge and leaderboard delta",
+ ],
+ };
+ }
+
+ if (!profile.orcidVerified) {
+ visibleFields.push("credit_role", "project_id", "unverified_profile_notice");
+ return {
+ ...base,
+ status: "citation_only",
+ visibilityDecision: "orcid_verification_required",
+ publishableReputationDelta: 0,
+ reasons: ["ORCID/profile identity is not verified"],
+ actions: [
+ "Limit credit to citation page until profile verification",
+ "Request ORCID verification before public reputation delta",
+ ],
+ };
+ }
+
+ visibleFields.push(
+ "display_name",
+ "orcid",
+ "credit_role",
+ "project_id",
+ "evidence_receipt_ids",
+ "reputation_delta"
+ );
+
+ return {
+ ...base,
+ status: "publishable",
+ visibilityDecision: "public_profile_and_citation",
+ publishableReputationDelta: base.reputationDelta,
+ reasons: [
+ "contributor consent is active",
+ "identity is verified",
+ "evidence receipts are immutable and public-safe",
+ ],
+ actions: [
+ "Publish credit on profile and citation page",
+ "Apply reputation delta to transparent profile ledger",
+ "Attach audit digest to profile update packet",
+ ],
+ };
+}
+
+function analyzeProfileCredits(profiles, options = {}) {
+ if (!Array.isArray(profiles)) {
+ throw new Error("profile packets must be an array");
+ }
+
+ const normalizedOptions = {
+ asOf: parseDate(options.asOf || new Date().toISOString(), "asOf").toISOString(),
+ };
+
+ const credits = [];
+ for (const profile of profiles) {
+ if (!Array.isArray(profile.credits)) {
+ throw new Error(`profile ${profile.researcherId || "(unknown)"} must include credits`);
+ }
+ for (const credit of profile.credits) {
+ credits.push(evaluateCredit(profile, credit, normalizedOptions));
+ }
+ }
+
+ const totals = credits.reduce(
+ (acc, credit) => {
+ acc.totalCredits += 1;
+ acc.publishableReputationDelta += credit.publishableReputationDelta;
+ acc.byStatus[credit.status] = (acc.byStatus[credit.status] || 0) + 1;
+ return acc;
+ },
+ {
+ totalCredits: 0,
+ publishableReputationDelta: 0,
+ byStatus: {},
+ }
+ );
+
+ const result = {
+ asOf: normalizedOptions.asOf,
+ totals,
+ credits,
+ };
+
+ return {
+ ...result,
+ auditDigest: createAuditDigest(result),
+ };
+}
+
+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 createAuditDigest(result) {
+ const payload = {
+ asOf: result.asOf,
+ totals: result.totals,
+ credits: result.credits.map((credit) => ({
+ id: credit.id,
+ researcherId: credit.researcherId,
+ status: credit.status,
+ visibilityDecision: credit.visibilityDecision,
+ publishableReputationDelta: credit.publishableReputationDelta,
+ reasons: credit.reasons,
+ })),
+ };
+
+ return crypto.createHash("sha256").update(stableStringify(payload)).digest("hex");
+}
+
+function renderMarkdownReport(result) {
+ const lines = [
+ "# Profile Credit Consent Visibility Guard",
+ "",
+ `As of: ${result.asOf}`,
+ `Audit digest: \`${result.auditDigest}\``,
+ "",
+ "## Totals",
+ "",
+ `- Credits evaluated: ${result.totals.totalCredits}`,
+ `- Publishable reputation delta: ${result.totals.publishableReputationDelta}`,
+ ];
+
+ for (const [status, count] of Object.entries(result.totals.byStatus).sort()) {
+ lines.push(`- ${status}: ${count}`);
+ }
+
+ lines.push("", "## Credit Decisions", "");
+ for (const credit of result.credits) {
+ lines.push(
+ `### ${credit.id}`,
+ "",
+ `- Researcher: ${credit.displayName} (${credit.researcherId})`,
+ `- Role: ${credit.creditRole}`,
+ `- Status: ${credit.status}`,
+ `- Visibility decision: ${credit.visibilityDecision}`,
+ `- Publishable reputation delta: ${credit.publishableReputationDelta}`,
+ `- Reasons: ${credit.reasons.join("; ")}`,
+ `- Actions: ${credit.actions.join("; ")}`,
+ ""
+ );
+ }
+
+ return `${lines.join("\n").trimEnd()}\n`;
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function renderSvgSummary(result) {
+ const statuses = Object.entries(result.totals.byStatus).sort();
+ const rows = statuses
+ .map(([status, count], index) => {
+ const y = 132 + index * 42;
+ const width = Math.max(32, count * 78);
+ return `
+ ${escapeXml(status)}
+
+ ${count}`;
+ })
+ .join("");
+
+ return `
+`;
+}
+
+module.exports = {
+ CREDIT_ROLES,
+ analyzeProfileCredits,
+ createAuditDigest,
+ renderMarkdownReport,
+ renderSvgSummary,
+};
diff --git a/profile-credit-consent-visibility-guard/package.json b/profile-credit-consent-visibility-guard/package.json
new file mode 100644
index 00000000..5251b9f4
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "profile-credit-consent-visibility-guard",
+ "version": "1.0.0",
+ "private": true,
+ "type": "commonjs",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js",
+ "test": "node test.js",
+ "demo": "node demo.js"
+ }
+}
diff --git a/profile-credit-consent-visibility-guard/reports/demo.mp4 b/profile-credit-consent-visibility-guard/reports/demo.mp4
new file mode 100644
index 00000000..428ae74a
Binary files /dev/null and b/profile-credit-consent-visibility-guard/reports/demo.mp4 differ
diff --git a/profile-credit-consent-visibility-guard/reports/reviewer-packet.md b/profile-credit-consent-visibility-guard/reports/reviewer-packet.md
new file mode 100644
index 00000000..9520bfdd
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/reports/reviewer-packet.md
@@ -0,0 +1,86 @@
+# Profile Credit Consent Visibility Guard
+
+As of: 2026-05-22T12:00:00.000Z
+Audit digest: `41de587fb911104fbd7898f0df8a48b004d188bf0c48b4084c8362c67143a6b5`
+
+## Totals
+
+- Credits evaluated: 7
+- Publishable reputation delta: 17
+- citation_only: 1
+- hold: 2
+- institutional_only: 1
+- publishable: 2
+- redacted: 1
+
+## Credit Decisions
+
+### credit-dataset-curation
+
+- Researcher: Ada Chen (researcher-ada)
+- Role: Data curation
+- Status: publishable
+- Visibility decision: public_profile_and_citation
+- Publishable reputation delta: 12
+- Reasons: contributor consent is active; identity is verified; evidence receipts are immutable and public-safe
+- Actions: Publish credit on profile and citation page; Apply reputation delta to transparent profile ledger; Attach audit digest to profile update packet
+
+### credit-open-review
+
+- Researcher: Ada Chen (researcher-ada)
+- Role: Reviewing
+- Status: publishable
+- Visibility decision: public_profile_and_citation
+- Publishable reputation delta: 5
+- Reasons: contributor consent is active; identity is verified; evidence receipts are immutable and public-safe
+- Actions: Publish credit on profile and citation page; Apply reputation delta to transparent profile ledger; Attach audit digest to profile update packet
+
+### credit-double-blind-review
+
+- Researcher: Blind Reviewer 7 (researcher-blind-reviewer)
+- Role: Reviewing
+- Status: redacted
+- Visibility decision: anonymous_until_release
+- Publishable reputation delta: 0
+- Reasons: double-blind review remains inside anonymous visibility window
+- Actions: Publish only anonymized review receipt; Hold profile and leaderboard reputation delta until anonymity release; Re-evaluate after 2026-08-01T00:00:00.000Z
+
+### credit-embargoed-code
+
+- Researcher: Bea Laurent (researcher-bea)
+- Role: Software
+- Status: citation_only
+- Visibility decision: embargoed_credit
+- Publishable reputation delta: 0
+- Reasons: linked artifact is still under embargo
+- Actions: Show citation-page placeholder only; Suppress public profile delta until embargo expires; Re-evaluate after 2026-07-15T00:00:00.000Z
+
+### credit-revoked-consent
+
+- Researcher: Cy Morgan (researcher-cy)
+- Role: Software
+- Status: hold
+- Visibility decision: do_not_publish
+- Publishable reputation delta: 0
+- Reasons: profile credit consent has been revoked
+- Actions: Remove credit from public profile surfaces; Retain private audit receipt only; Ask contributor to refresh consent in account settings
+
+### credit-private-protocol
+
+- Researcher: Dev Imani (researcher-dev)
+- Role: Methodology
+- Status: institutional_only
+- Visibility decision: restricted_export_only
+- Publishable reputation delta: 0
+- Reasons: credit evidence is restricted to institutional export
+- Actions: Exclude from public profile and citation page; Include in institutional report with restricted evidence label; Require steward approval before public conversion
+
+### credit-missing-evidence
+
+- Researcher: Eli Novak (researcher-eli)
+- Role: Supervision
+- Status: hold
+- Visibility decision: evidence_review
+- Publishable reputation delta: 0
+- Reasons: credit has no evidence receipt ids; source receipt is not immutable
+- Actions: Block reputation delta from profile and leaderboard; Attach missing evidence before publication; Route credit receipt to steward review
diff --git a/profile-credit-consent-visibility-guard/reports/summary.json b/profile-credit-consent-visibility-guard/reports/summary.json
new file mode 100644
index 00000000..8f1046f4
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/reports/summary.json
@@ -0,0 +1,216 @@
+{
+ "asOf": "2026-05-22T12:00:00.000Z",
+ "totals": {
+ "totalCredits": 7,
+ "publishableReputationDelta": 17,
+ "byStatus": {
+ "publishable": 2,
+ "redacted": 1,
+ "citation_only": 1,
+ "hold": 2,
+ "institutional_only": 1
+ }
+ },
+ "credits": [
+ {
+ "id": "credit-dataset-curation",
+ "researcherId": "researcher-ada",
+ "displayName": "Ada Chen",
+ "projectId": "proj-open-cell-atlas",
+ "creditType": "dataset_curation",
+ "creditRole": "Data curation",
+ "requestedVisibility": "public",
+ "targetSurface": "profile_and_citation",
+ "artifactVisibility": "public",
+ "reputationDelta": 12,
+ "publishableReputationDelta": 12,
+ "visibleFields": [
+ "display_name",
+ "orcid",
+ "credit_role",
+ "project_id",
+ "evidence_receipt_ids",
+ "reputation_delta"
+ ],
+ "reasons": [
+ "contributor consent is active",
+ "identity is verified",
+ "evidence receipts are immutable and public-safe"
+ ],
+ "actions": [
+ "Publish credit on profile and citation page",
+ "Apply reputation delta to transparent profile ledger",
+ "Attach audit digest to profile update packet"
+ ],
+ "status": "publishable",
+ "visibilityDecision": "public_profile_and_citation"
+ },
+ {
+ "id": "credit-open-review",
+ "researcherId": "researcher-ada",
+ "displayName": "Ada Chen",
+ "projectId": "proj-open-cell-atlas",
+ "creditType": "peer_review",
+ "creditRole": "Reviewing",
+ "requestedVisibility": "public",
+ "targetSurface": "profile",
+ "artifactVisibility": "public",
+ "reputationDelta": 5,
+ "publishableReputationDelta": 5,
+ "visibleFields": [
+ "display_name",
+ "orcid",
+ "credit_role",
+ "project_id",
+ "evidence_receipt_ids",
+ "reputation_delta"
+ ],
+ "reasons": [
+ "contributor consent is active",
+ "identity is verified",
+ "evidence receipts are immutable and public-safe"
+ ],
+ "actions": [
+ "Publish credit on profile and citation page",
+ "Apply reputation delta to transparent profile ledger",
+ "Attach audit digest to profile update packet"
+ ],
+ "status": "publishable",
+ "visibilityDecision": "public_profile_and_citation"
+ },
+ {
+ "id": "credit-double-blind-review",
+ "researcherId": "researcher-blind-reviewer",
+ "displayName": "Blind Reviewer 7",
+ "projectId": "proj-novel-assay",
+ "creditType": "peer_review",
+ "creditRole": "Reviewing",
+ "requestedVisibility": "public",
+ "targetSurface": "profile_and_leaderboard",
+ "artifactVisibility": "restricted",
+ "reputationDelta": 9,
+ "publishableReputationDelta": 0,
+ "visibleFields": [
+ "anonymous_reviewer_label",
+ "review_type",
+ "project_id"
+ ],
+ "reasons": [
+ "double-blind review remains inside anonymous visibility window"
+ ],
+ "actions": [
+ "Publish only anonymized review receipt",
+ "Hold profile and leaderboard reputation delta until anonymity release",
+ "Re-evaluate after 2026-08-01T00:00:00.000Z"
+ ],
+ "status": "redacted",
+ "visibilityDecision": "anonymous_until_release"
+ },
+ {
+ "id": "credit-embargoed-code",
+ "researcherId": "researcher-bea",
+ "displayName": "Bea Laurent",
+ "projectId": "proj-trial-protocol",
+ "creditType": "software",
+ "creditRole": "Software",
+ "requestedVisibility": "citation_only",
+ "targetSurface": "citation_page",
+ "artifactVisibility": "restricted",
+ "reputationDelta": 8,
+ "publishableReputationDelta": 0,
+ "visibleFields": [
+ "credit_role",
+ "project_id",
+ "embargo_label"
+ ],
+ "reasons": [
+ "linked artifact is still under embargo"
+ ],
+ "actions": [
+ "Show citation-page placeholder only",
+ "Suppress public profile delta until embargo expires",
+ "Re-evaluate after 2026-07-15T00:00:00.000Z"
+ ],
+ "status": "citation_only",
+ "visibilityDecision": "embargoed_credit"
+ },
+ {
+ "id": "credit-revoked-consent",
+ "researcherId": "researcher-cy",
+ "displayName": "Cy Morgan",
+ "projectId": "proj-open-cell-atlas",
+ "creditType": "software",
+ "creditRole": "Software",
+ "requestedVisibility": "public",
+ "targetSurface": "profile",
+ "artifactVisibility": "public",
+ "reputationDelta": 7,
+ "publishableReputationDelta": 0,
+ "visibleFields": [],
+ "reasons": [
+ "profile credit consent has been revoked"
+ ],
+ "actions": [
+ "Remove credit from public profile surfaces",
+ "Retain private audit receipt only",
+ "Ask contributor to refresh consent in account settings"
+ ],
+ "status": "hold",
+ "visibilityDecision": "do_not_publish"
+ },
+ {
+ "id": "credit-private-protocol",
+ "researcherId": "researcher-dev",
+ "displayName": "Dev Imani",
+ "projectId": "proj-private-protocol",
+ "creditType": "methodology",
+ "creditRole": "Methodology",
+ "requestedVisibility": "institutional_only",
+ "targetSurface": "institutional_export",
+ "artifactVisibility": "private",
+ "reputationDelta": 6,
+ "publishableReputationDelta": 0,
+ "visibleFields": [
+ "credit_role",
+ "project_id",
+ "institutional_receipt_id"
+ ],
+ "reasons": [
+ "credit evidence is restricted to institutional export"
+ ],
+ "actions": [
+ "Exclude from public profile and citation page",
+ "Include in institutional report with restricted evidence label",
+ "Require steward approval before public conversion"
+ ],
+ "status": "institutional_only",
+ "visibilityDecision": "restricted_export_only"
+ },
+ {
+ "id": "credit-missing-evidence",
+ "researcherId": "researcher-eli",
+ "displayName": "Eli Novak",
+ "projectId": "proj-literature-map",
+ "creditType": "supervision",
+ "creditRole": "Supervision",
+ "requestedVisibility": "public",
+ "targetSurface": "profile",
+ "artifactVisibility": "public",
+ "reputationDelta": 4,
+ "publishableReputationDelta": 0,
+ "visibleFields": [],
+ "reasons": [
+ "credit has no evidence receipt ids",
+ "source receipt is not immutable"
+ ],
+ "actions": [
+ "Block reputation delta from profile and leaderboard",
+ "Attach missing evidence before publication",
+ "Route credit receipt to steward review"
+ ],
+ "status": "hold",
+ "visibilityDecision": "evidence_review"
+ }
+ ],
+ "auditDigest": "41de587fb911104fbd7898f0df8a48b004d188bf0c48b4084c8362c67143a6b5"
+}
diff --git a/profile-credit-consent-visibility-guard/reports/summary.svg b/profile-credit-consent-visibility-guard/reports/summary.svg
new file mode 100644
index 00000000..4ecf4a95
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/reports/summary.svg
@@ -0,0 +1,33 @@
+
diff --git a/profile-credit-consent-visibility-guard/requirements-map.md b/profile-credit-consent-visibility-guard/requirements-map.md
new file mode 100644
index 00000000..baf50634
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/requirements-map.md
@@ -0,0 +1,24 @@
+# Requirements Map
+
+## Issue #15: Community & User Reputation System
+
+| Requirement area | Coverage in this module |
+| --- | --- |
+| Contributor credits | Evaluates CRediT-style credit records before profile or citation publication. |
+| Visible credit on researcher profiles and citation pages | Separates public profile, citation-only, redacted, institutional-only, and hold decisions. |
+| Review history tracked on profiles | Allows open review receipts while redacting double-blind review identity until release. |
+| Public, semi-private, and anonymous review modes | Keeps anonymous and double-blind credits redacted while preserving audit receipts. |
+| Reputation scoring | Applies reputation deltas only when consent, evidence, identity, and artifact visibility are safe. |
+| Institutional reporting | Emits institutional-only decisions for private/sensitive artifacts instead of public exposure. |
+| Trust and transparency | Produces deterministic reasons, actions, visible fields, and an audit digest for each credit. |
+
+## Non-Overlap
+
+This is not another broad reputation ledger, endorsement-ring guard, mentorship
+ladder, correction-impact ledger, credit attestation module, peer-review
+calibration module, COI assignment checker, transparency receipt ledger,
+leaderboard eligibility guard, badge renewal gate, civility gate, review
+timeliness guard, peer-review recusal guard, or edit-history integrity guard.
+
+It focuses only on whether already-recorded contribution and review credit may
+be shown on profile, citation, leaderboard, or institutional surfaces.
diff --git a/profile-credit-consent-visibility-guard/sample-data.js b/profile-credit-consent-visibility-guard/sample-data.js
new file mode 100644
index 00000000..3e599dbd
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/sample-data.js
@@ -0,0 +1,171 @@
+const sampleProfileCreditPackets = [
+ {
+ researcherId: "researcher-ada",
+ displayName: "Ada Chen",
+ orcid: "0000-0002-1825-0097",
+ orcidVerified: true,
+ profileConsent: {
+ status: "granted",
+ grantedAt: "2026-03-01T09:00:00.000Z",
+ visibility: "public",
+ },
+ credits: [
+ {
+ id: "credit-dataset-curation",
+ projectId: "proj-open-cell-atlas",
+ creditType: "dataset_curation",
+ creditRole: "Data curation",
+ targetSurface: "profile_and_citation",
+ requestedVisibility: "public",
+ artifactVisibility: "public",
+ evidenceIds: ["dataset-doi-10.5555/cell-atlas-v2", "curation-commit-a1"],
+ sourceReceiptImmutable: true,
+ reputationDelta: 12,
+ },
+ {
+ id: "credit-open-review",
+ projectId: "proj-open-cell-atlas",
+ creditType: "peer_review",
+ creditRole: "Reviewing",
+ targetSurface: "profile",
+ requestedVisibility: "public",
+ artifactVisibility: "public",
+ evidenceIds: ["review-receipt-204"],
+ sourceReceiptImmutable: true,
+ reputationDelta: 5,
+ },
+ ],
+ },
+ {
+ researcherId: "researcher-blind-reviewer",
+ displayName: "Blind Reviewer 7",
+ orcid: "0000-0003-1415-9265",
+ orcidVerified: true,
+ profileConsent: {
+ status: "granted",
+ grantedAt: "2026-02-15T12:00:00.000Z",
+ visibility: "public",
+ },
+ credits: [
+ {
+ id: "credit-double-blind-review",
+ projectId: "proj-novel-assay",
+ creditType: "peer_review",
+ creditRole: "Reviewing",
+ targetSurface: "profile_and_leaderboard",
+ requestedVisibility: "public",
+ artifactVisibility: "restricted",
+ reviewMode: "double_blind",
+ anonymousUntil: "2026-08-01T00:00:00.000Z",
+ evidenceIds: ["blind-review-receipt-11"],
+ sourceReceiptImmutable: true,
+ reputationDelta: 9,
+ },
+ ],
+ },
+ {
+ researcherId: "researcher-bea",
+ displayName: "Bea Laurent",
+ orcid: "0000-0001-7777-2222",
+ orcidVerified: true,
+ profileConsent: {
+ status: "granted",
+ grantedAt: "2026-01-05T09:00:00.000Z",
+ visibility: "citation_only",
+ },
+ credits: [
+ {
+ id: "credit-embargoed-code",
+ projectId: "proj-trial-protocol",
+ creditType: "software",
+ creditRole: "Software",
+ targetSurface: "citation_page",
+ requestedVisibility: "citation_only",
+ artifactVisibility: "restricted",
+ embargoUntil: "2026-07-15T00:00:00.000Z",
+ evidenceIds: ["code-commit-embargoed-44"],
+ sourceReceiptImmutable: true,
+ reputationDelta: 8,
+ },
+ ],
+ },
+ {
+ researcherId: "researcher-cy",
+ displayName: "Cy Morgan",
+ orcid: "0000-0002-8888-3333",
+ orcidVerified: true,
+ profileConsent: {
+ status: "revoked",
+ grantedAt: "2025-12-01T09:00:00.000Z",
+ revokedAt: "2026-04-20T09:00:00.000Z",
+ visibility: "public",
+ },
+ credits: [
+ {
+ id: "credit-revoked-consent",
+ projectId: "proj-open-cell-atlas",
+ creditType: "software",
+ creditRole: "Software",
+ targetSurface: "profile",
+ requestedVisibility: "public",
+ artifactVisibility: "public",
+ evidenceIds: ["commit-cy-88"],
+ sourceReceiptImmutable: true,
+ reputationDelta: 7,
+ },
+ ],
+ },
+ {
+ researcherId: "researcher-dev",
+ displayName: "Dev Imani",
+ orcid: "0000-0002-9999-4444",
+ orcidVerified: false,
+ profileConsent: {
+ status: "granted",
+ grantedAt: "2026-03-10T09:00:00.000Z",
+ visibility: "institutional_only",
+ },
+ credits: [
+ {
+ id: "credit-private-protocol",
+ projectId: "proj-private-protocol",
+ creditType: "methodology",
+ creditRole: "Methodology",
+ targetSurface: "institutional_export",
+ requestedVisibility: "institutional_only",
+ artifactVisibility: "private",
+ sensitiveEvidence: true,
+ evidenceIds: ["dua-controlled-protocol-9"],
+ sourceReceiptImmutable: true,
+ reputationDelta: 6,
+ },
+ ],
+ },
+ {
+ researcherId: "researcher-eli",
+ displayName: "Eli Novak",
+ orcid: "0000-0002-5555-6666",
+ orcidVerified: true,
+ profileConsent: {
+ status: "granted",
+ grantedAt: "2026-03-22T09:00:00.000Z",
+ visibility: "public",
+ },
+ credits: [
+ {
+ id: "credit-missing-evidence",
+ projectId: "proj-literature-map",
+ creditType: "supervision",
+ creditRole: "Supervision",
+ targetSurface: "profile",
+ requestedVisibility: "public",
+ artifactVisibility: "public",
+ evidenceIds: [],
+ sourceReceiptImmutable: false,
+ reputationDelta: 4,
+ },
+ ],
+ },
+];
+
+module.exports = { sampleProfileCreditPackets };
diff --git a/profile-credit-consent-visibility-guard/test.js b/profile-credit-consent-visibility-guard/test.js
new file mode 100644
index 00000000..0eea1c24
--- /dev/null
+++ b/profile-credit-consent-visibility-guard/test.js
@@ -0,0 +1,87 @@
+const assert = require("node:assert/strict");
+const {
+ analyzeProfileCredits,
+ createAuditDigest,
+} = require("./index");
+const { sampleProfileCreditPackets } = require("./sample-data");
+
+const AS_OF = "2026-05-22T12:00:00.000Z";
+
+function byId(result, id) {
+ const credit = result.credits.find((entry) => entry.id === id);
+ assert.ok(credit, `expected credit ${id} to exist`);
+ return credit;
+}
+
+function run() {
+ const result = analyzeProfileCredits(sampleProfileCreditPackets, {
+ asOf: AS_OF,
+ });
+
+ assert.equal(result.asOf, AS_OF);
+ assert.equal(result.totals.totalCredits, 7);
+ assert.equal(result.totals.publishableReputationDelta, 17);
+ assert.equal(result.totals.byStatus.publishable, 2);
+ assert.equal(result.totals.byStatus.redacted, 1);
+ assert.equal(result.totals.byStatus.citation_only, 1);
+ assert.equal(result.totals.byStatus.institutional_only, 1);
+ assert.equal(result.totals.byStatus.hold, 2);
+
+ const dataset = byId(result, "credit-dataset-curation");
+ assert.equal(dataset.status, "publishable");
+ assert.equal(dataset.visibilityDecision, "public_profile_and_citation");
+ assert.equal(dataset.publishableReputationDelta, 12);
+ assert.ok(dataset.visibleFields.includes("orcid"));
+
+ const openReview = byId(result, "credit-open-review");
+ assert.equal(openReview.status, "publishable");
+ assert.equal(openReview.publishableReputationDelta, 5);
+
+ const blind = byId(result, "credit-double-blind-review");
+ assert.equal(blind.status, "redacted");
+ assert.equal(blind.publishableReputationDelta, 0);
+ assert.ok(blind.visibleFields.includes("anonymous_reviewer_label"));
+ assert.ok(blind.reasons.includes("double-blind review remains inside anonymous visibility window"));
+
+ const embargoed = byId(result, "credit-embargoed-code");
+ assert.equal(embargoed.status, "citation_only");
+ assert.equal(embargoed.visibilityDecision, "embargoed_credit");
+ assert.ok(embargoed.actions.some((action) => action.includes("2026-07-15")));
+
+ const revoked = byId(result, "credit-revoked-consent");
+ assert.equal(revoked.status, "hold");
+ assert.equal(revoked.visibilityDecision, "do_not_publish");
+ assert.ok(revoked.reasons.includes("profile credit consent has been revoked"));
+
+ const privateProtocol = byId(result, "credit-private-protocol");
+ assert.equal(privateProtocol.status, "institutional_only");
+ assert.equal(privateProtocol.visibilityDecision, "restricted_export_only");
+ assert.equal(privateProtocol.publishableReputationDelta, 0);
+
+ const missing = byId(result, "credit-missing-evidence");
+ assert.equal(missing.status, "hold");
+ assert.equal(missing.visibilityDecision, "evidence_review");
+ assert.ok(missing.reasons.includes("credit has no evidence receipt ids"));
+ assert.ok(missing.reasons.includes("source receipt is not immutable"));
+
+ const digest = createAuditDigest(result);
+ assert.match(digest, /^[a-f0-9]{64}$/);
+ assert.equal(digest, createAuditDigest(result));
+
+ assert.throws(
+ () =>
+ analyzeProfileCredits(
+ [{ researcherId: "bad", displayName: "Bad", profileConsent: {} }],
+ { asOf: AS_OF }
+ ),
+ /must include credits/
+ );
+
+ assert.throws(
+ () => analyzeProfileCredits([{ researcherId: "bad", credits: [{}] }], { asOf: AS_OF }),
+ /missing required profile field/
+ );
+}
+
+run();
+console.log("profile credit consent visibility guard tests passed");