diff --git a/project-funding-attribution-guard/README.md b/project-funding-attribution-guard/README.md
new file mode 100644
index 00000000..d64e3fa2
--- /dev/null
+++ b/project-funding-attribution-guard/README.md
@@ -0,0 +1,36 @@
+# Project Funding Attribution Guard
+
+A focused User & Project Management slice for checking whether a research project has valid funding-source, institution, collaborator, and citation attribution before publication or public profile exposure.
+
+The module is deterministic, dependency-free, and uses synthetic data only. It does not handle payout routing, billing, payment rails, or private financial details.
+
+## What It Checks
+
+- Funding sources linked to project metadata and required acknowledgements.
+- Institution ownership and collaborator affiliation consistency.
+- Grant IDs and sponsor terms required for public project pages.
+- Public/private profile exposure against grant and institution requirements.
+- Citation metadata readiness for DOI/project exports.
+- Audit evidence for project-level attribution decisions.
+- Reviewer-facing remediation actions before publication.
+
+## Usage
+
+```bash
+node project-funding-attribution-guard/test.js
+node project-funding-attribution-guard/demo.js
+```
+
+The demo writes:
+
+- `reports/attribution-audit.json`
+- `reports/reviewer-packet.md`
+- `reports/attribution-summary.svg`
+
+The PR includes a short demo video at `reports/demo.mp4`.
+
+## Maintainer-Friendly Notes
+
+- No external services, credentials, network calls, or package installs.
+- All fixtures are synthetic and intentionally small.
+- The guard complements identity/access modules by focusing specifically on project-level funding and institutional attribution readiness.
diff --git a/project-funding-attribution-guard/demo.js b/project-funding-attribution-guard/demo.js
new file mode 100644
index 00000000..a46e6a7b
--- /dev/null
+++ b/project-funding-attribution-guard/demo.js
@@ -0,0 +1,23 @@
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+const {
+ evaluateProjectFundingAttribution,
+ renderReviewerMarkdown,
+ renderSvgSummary
+} = require("./index");
+const { project } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const audit = evaluateProjectFundingAttribution(project);
+
+fs.writeFileSync(path.join(reportsDir, "attribution-audit.json"), `${JSON.stringify(audit, null, 2)}\n`);
+fs.writeFileSync(path.join(reportsDir, "reviewer-packet.md"), renderReviewerMarkdown(audit));
+fs.writeFileSync(path.join(reportsDir, "attribution-summary.svg"), renderSvgSummary(audit));
+
+console.log(`Readiness: ${audit.readiness} (${audit.readinessScore}/100)`);
+console.log(`Findings: ${audit.findings.map((finding) => finding.code).join(", ")}`);
+console.log(`Reports written to ${reportsDir}`);
diff --git a/project-funding-attribution-guard/index.js b/project-funding-attribution-guard/index.js
new file mode 100644
index 00000000..9067e52d
--- /dev/null
+++ b/project-funding-attribution-guard/index.js
@@ -0,0 +1,247 @@
+"use strict";
+
+function byId(items) {
+ return Object.fromEntries(items.map((item) => [item.id, item]));
+}
+
+function unique(values) {
+ return [...new Set(values.filter(Boolean))];
+}
+
+function hasText(haystack, needle) {
+ return String(haystack || "").toLowerCase().includes(String(needle || "").toLowerCase());
+}
+
+function evaluateFunding(project) {
+ const acknowledgement = project.citationMetadata.fundingAcknowledgement || "";
+ return project.fundingSources.map((source) => {
+ const missingGrantId = !source.grantId;
+ const missingAcknowledgement = source.publicAcknowledgementRequired && !hasText(acknowledgement, source.requiredText);
+ const missingDoi = source.requiresDoiBeforePublication && !project.citationMetadata.doi;
+ const evidence = project.auditEvents.filter((event) => event.targetId === source.id);
+ const blockers = [];
+
+ if (missingGrantId) blockers.push("grant id missing");
+ if (missingAcknowledgement) blockers.push("required acknowledgement missing");
+ if (missingDoi) blockers.push("DOI required before publication");
+ if (!evidence.length) blockers.push("funding link lacks audit evidence");
+
+ return {
+ fundingSourceId: source.id,
+ funder: source.funder,
+ grantId: source.grantId || "missing",
+ readiness: blockers.length ? "blocked" : "ready",
+ blockers,
+ auditEventIds: evidence.map((event) => event.id)
+ };
+ });
+}
+
+function evaluateInstitutions(project) {
+ const institutionMap = byId(project.linkedInstitutions);
+ return project.collaborators.map((collaborator) => {
+ const institution = institutionMap[collaborator.institutionId];
+ const blockers = [];
+ if (!institution) {
+ blockers.push("linked institution missing");
+ } else if (collaborator.emailDomain !== institution.verifiedDomain) {
+ blockers.push("email domain does not match institution domain");
+ }
+ if (!collaborator.orcidLinked && ["owner", "contributor"].includes(collaborator.role)) {
+ blockers.push("ORCID link missing for attributed contributor");
+ }
+ return {
+ collaboratorId: collaborator.id,
+ name: collaborator.name,
+ role: collaborator.role,
+ institution: institution ? institution.name : "missing",
+ readiness: blockers.length ? "needs-review" : "ready",
+ blockers
+ };
+ });
+}
+
+function evaluateCitationMetadata(project) {
+ const blockers = [];
+ const warnings = [];
+ const institutions = project.linkedInstitutions.map((institution) => institution.name);
+ const authors = project.collaborators
+ .filter((collaborator) => ["owner", "contributor"].includes(collaborator.role))
+ .map((collaborator) => collaborator.name);
+
+ if (!project.citationMetadata.title) blockers.push("citation title missing");
+ if (!project.citationMetadata.doi) warnings.push("DOI missing for export-ready citation metadata");
+ const missingAuthors = authors.filter((author) => !(project.citationMetadata.authors || []).includes(author));
+ if (missingAuthors.length) blockers.push(`citation authors missing: ${missingAuthors.join(", ")}`);
+ const missingInstitutions = institutions.filter((institution) => !(project.citationMetadata.institutions || []).includes(institution));
+ if (missingInstitutions.length) warnings.push(`citation institutions missing: ${missingInstitutions.join(", ")}`);
+
+ return {
+ readiness: blockers.length ? "blocked" : warnings.length ? "ready-with-warnings" : "ready",
+ blockers,
+ warnings
+ };
+}
+
+function evaluateProfileExposure(project, institutionEvaluations, fundingEvaluations) {
+ const findings = [];
+ const publicProject = project.visibility === "public";
+ const blockedFunding = fundingEvaluations.filter((item) => item.readiness === "blocked");
+ const exposedReviewers = project.collaborators.filter((collaborator) => collaborator.role === "reviewer" && collaborator.publicProfile);
+ const affiliationIssues = institutionEvaluations.filter((item) => item.blockers.length);
+
+ if (publicProject && blockedFunding.length) {
+ findings.push({
+ severity: "high",
+ code: "public-project-funding-blocker",
+ message: "Public project visibility is blocked by incomplete funding attribution.",
+ evidence: blockedFunding.map((item) => `${item.fundingSourceId}: ${item.blockers.join("; ")}`),
+ action: "Complete funding acknowledgements, grant IDs, DOI requirements, and audit evidence before public release."
+ });
+ }
+ if (exposedReviewers.length) {
+ findings.push({
+ severity: "medium",
+ code: "reviewer-profile-exposure",
+ message: "Reviewer profile exposure should be checked before public project display.",
+ evidence: exposedReviewers.map((collaborator) => collaborator.name),
+ action: "Confirm reviewer profile visibility matches project policy before publication."
+ });
+ }
+ if (affiliationIssues.length) {
+ findings.push({
+ severity: "medium",
+ code: "affiliation-mismatch",
+ message: "Some collaborators have institution attribution issues.",
+ evidence: affiliationIssues.map((item) => `${item.name}: ${item.blockers.join("; ")}`),
+ action: "Resolve ORCID and institution-domain mismatches before final attribution."
+ });
+ }
+
+ return findings;
+}
+
+function evaluateProjectFundingAttribution(project) {
+ const funding = evaluateFunding(project);
+ const institutions = evaluateInstitutions(project);
+ const citation = evaluateCitationMetadata(project);
+ const profileFindings = evaluateProfileExposure(project, institutions, funding);
+ const findings = [...profileFindings];
+
+ if (citation.readiness === "blocked") {
+ findings.push({
+ severity: "high",
+ code: "citation-metadata-blocked",
+ message: "Project citation metadata is missing required attribution fields.",
+ evidence: citation.blockers,
+ action: "Complete author and citation metadata before DOI or public export."
+ });
+ }
+ if (citation.warnings.length) {
+ findings.push({
+ severity: "low",
+ code: "citation-metadata-warning",
+ message: "Citation metadata is usable but not export-ready.",
+ evidence: citation.warnings,
+ action: "Resolve citation warnings before repository export or DOI registration."
+ });
+ }
+
+ const blockedFunding = funding.filter((item) => item.readiness === "blocked").length;
+ const institutionIssues = institutions.filter((item) => item.readiness !== "ready").length;
+ const readinessScore = Math.max(
+ 0,
+ Math.min(
+ 100,
+ 100 -
+ blockedFunding * 18 -
+ institutionIssues * 12 -
+ (citation.readiness === "blocked" ? 20 : 0) -
+ citation.warnings.length * 6 -
+ findings.filter((finding) => finding.severity === "high").length * 8
+ )
+ );
+
+ return {
+ projectId: project.id,
+ generatedBy: "project-funding-attribution-guard",
+ readiness: readinessScore >= 78 ? "ready-with-review" : "needs-attribution-fixes",
+ readinessScore,
+ metrics: {
+ fundingSources: funding.length,
+ blockedFundingSources: blockedFunding,
+ collaborators: institutions.length,
+ collaboratorAttributionIssues: institutionIssues,
+ findings: findings.length
+ },
+ funding,
+ institutions,
+ citation,
+ findings,
+ reviewerActions: unique(findings.map((finding) => finding.action)),
+ auditDigest: [
+ `${funding.length} funding sources evaluated`,
+ `${blockedFunding} blocked funding sources`,
+ `${institutions.length} collaborator affiliations evaluated`,
+ `${institutionIssues} collaborator attribution issues`,
+ `${findings.length} reviewer findings`
+ ]
+ };
+}
+
+function renderReviewerMarkdown(audit) {
+ const lines = [
+ "# Project Funding Attribution Guard",
+ "",
+ `Project: ${audit.projectId}`,
+ `Readiness: ${audit.readiness} (${audit.readinessScore}/100)`,
+ "",
+ "## Metrics",
+ "",
+ `- Funding sources: ${audit.metrics.fundingSources}`,
+ `- Blocked funding sources: ${audit.metrics.blockedFundingSources}`,
+ `- Collaborators: ${audit.metrics.collaborators}`,
+ `- Attribution issues: ${audit.metrics.collaboratorAttributionIssues}`,
+ "",
+ "## Findings",
+ ""
+ ];
+ audit.findings.forEach((finding) => {
+ lines.push(`- [${finding.severity}] ${finding.code}: ${finding.message}`);
+ lines.push(` Action: ${finding.action}`);
+ });
+ lines.push("", "## Funding Sources", "");
+ audit.funding.forEach((source) => {
+ lines.push(`- ${source.fundingSourceId}: ${source.readiness} (${source.blockers.join("; ") || "no blockers"})`);
+ });
+ return `${lines.join("\n")}\n`;
+}
+
+function renderSvgSummary(audit) {
+ const barWidth = Math.round((audit.readinessScore / 100) * 760);
+ const findingLines = audit.findings.slice(0, 5).map((finding, index) => {
+ const y = 318 + index * 34;
+ return `${finding.severity.toUpperCase()} ${finding.code}`;
+ }).join("\n");
+
+ return `
+`;
+}
+
+module.exports = {
+ evaluateProjectFundingAttribution,
+ renderReviewerMarkdown,
+ renderSvgSummary,
+ evaluateFunding,
+ evaluateInstitutions,
+ evaluateCitationMetadata
+};
diff --git a/project-funding-attribution-guard/reports/attribution-audit.json b/project-funding-attribution-guard/reports/attribution-audit.json
new file mode 100644
index 00000000..936d852c
--- /dev/null
+++ b/project-funding-attribution-guard/reports/attribution-audit.json
@@ -0,0 +1,121 @@
+{
+ "projectId": "proj-neuro-air-001",
+ "generatedBy": "project-funding-attribution-guard",
+ "readiness": "needs-attribution-fixes",
+ "readinessScore": 20,
+ "metrics": {
+ "fundingSources": 2,
+ "blockedFundingSources": 2,
+ "collaborators": 3,
+ "collaboratorAttributionIssues": 2,
+ "findings": 3
+ },
+ "funding": [
+ {
+ "fundingSourceId": "grant-clean-cities",
+ "funder": "Clean Cities Health Initiative",
+ "grantId": "CCHI-2026-118",
+ "readiness": "blocked",
+ "blockers": [
+ "DOI required before publication"
+ ],
+ "auditEventIds": [
+ "ae-1"
+ ]
+ },
+ {
+ "fundingSourceId": "grant-pediatric-data",
+ "funder": "Pediatric Data Commons",
+ "grantId": "missing",
+ "readiness": "blocked",
+ "blockers": [
+ "grant id missing",
+ "required acknowledgement missing",
+ "funding link lacks audit evidence"
+ ],
+ "auditEventIds": []
+ }
+ ],
+ "institutions": [
+ {
+ "collaboratorId": "u-ada",
+ "name": "Ada Chen",
+ "role": "owner",
+ "institution": "North Valley University",
+ "readiness": "ready",
+ "blockers": []
+ },
+ {
+ "collaboratorId": "u-luis",
+ "name": "Luis Ortega",
+ "role": "contributor",
+ "institution": "Bridge Children's Hospital",
+ "readiness": "needs-review",
+ "blockers": [
+ "ORCID link missing for attributed contributor"
+ ]
+ },
+ {
+ "collaboratorId": "u-mina",
+ "name": "Mina Patel",
+ "role": "reviewer",
+ "institution": "North Valley University",
+ "readiness": "needs-review",
+ "blockers": [
+ "email domain does not match institution domain"
+ ]
+ }
+ ],
+ "citation": {
+ "readiness": "ready-with-warnings",
+ "blockers": [],
+ "warnings": [
+ "DOI missing for export-ready citation metadata",
+ "citation institutions missing: Bridge Children's Hospital"
+ ]
+ },
+ "findings": [
+ {
+ "severity": "high",
+ "code": "public-project-funding-blocker",
+ "message": "Public project visibility is blocked by incomplete funding attribution.",
+ "evidence": [
+ "grant-clean-cities: DOI required before publication",
+ "grant-pediatric-data: grant id missing; required acknowledgement missing; funding link lacks audit evidence"
+ ],
+ "action": "Complete funding acknowledgements, grant IDs, DOI requirements, and audit evidence before public release."
+ },
+ {
+ "severity": "medium",
+ "code": "affiliation-mismatch",
+ "message": "Some collaborators have institution attribution issues.",
+ "evidence": [
+ "Luis Ortega: ORCID link missing for attributed contributor",
+ "Mina Patel: email domain does not match institution domain"
+ ],
+ "action": "Resolve ORCID and institution-domain mismatches before final attribution."
+ },
+ {
+ "severity": "low",
+ "code": "citation-metadata-warning",
+ "message": "Citation metadata is usable but not export-ready.",
+ "evidence": [
+ "DOI missing for export-ready citation metadata",
+ "citation institutions missing: Bridge Children's Hospital"
+ ],
+ "action": "Resolve citation warnings before repository export or DOI registration."
+ }
+ ],
+ "reviewerActions": [
+ "Complete funding acknowledgements, grant IDs, DOI requirements, and audit evidence before public release.",
+ "Resolve ORCID and institution-domain mismatches before final attribution.",
+ "Resolve citation warnings before repository export or DOI registration."
+ ],
+ "auditDigest": [
+ "2 funding sources evaluated",
+ "2 blocked funding sources",
+ "3 collaborator affiliations evaluated",
+ "2 collaborator attribution issues",
+ "3 reviewer findings"
+ ]
+}
diff --git a/project-funding-attribution-guard/reports/attribution-summary.svg b/project-funding-attribution-guard/reports/attribution-summary.svg
new file mode 100644
index 00000000..23d9a8cc
--- /dev/null
+++ b/project-funding-attribution-guard/reports/attribution-summary.svg
@@ -0,0 +1,12 @@
+
diff --git a/project-funding-attribution-guard/reports/demo.mp4 b/project-funding-attribution-guard/reports/demo.mp4
new file mode 100644
index 00000000..92af3cd0
Binary files /dev/null and b/project-funding-attribution-guard/reports/demo.mp4 differ
diff --git a/project-funding-attribution-guard/reports/reviewer-packet.md b/project-funding-attribution-guard/reports/reviewer-packet.md
new file mode 100644
index 00000000..c83942b7
--- /dev/null
+++ b/project-funding-attribution-guard/reports/reviewer-packet.md
@@ -0,0 +1,25 @@
+# Project Funding Attribution Guard
+
+Project: proj-neuro-air-001
+Readiness: needs-attribution-fixes (20/100)
+
+## Metrics
+
+- Funding sources: 2
+- Blocked funding sources: 2
+- Collaborators: 3
+- Attribution issues: 2
+
+## Findings
+
+- [high] public-project-funding-blocker: Public project visibility is blocked by incomplete funding attribution.
+ Action: Complete funding acknowledgements, grant IDs, DOI requirements, and audit evidence before public release.
+- [medium] affiliation-mismatch: Some collaborators have institution attribution issues.
+ Action: Resolve ORCID and institution-domain mismatches before final attribution.
+- [low] citation-metadata-warning: Citation metadata is usable but not export-ready.
+ Action: Resolve citation warnings before repository export or DOI registration.
+
+## Funding Sources
+
+- grant-clean-cities: blocked (DOI required before publication)
+- grant-pediatric-data: blocked (grant id missing; required acknowledgement missing; funding link lacks audit evidence)
diff --git a/project-funding-attribution-guard/sample-data.js b/project-funding-attribution-guard/sample-data.js
new file mode 100644
index 00000000..a22e675f
--- /dev/null
+++ b/project-funding-attribution-guard/sample-data.js
@@ -0,0 +1,104 @@
+"use strict";
+
+const project = {
+ id: "proj-neuro-air-001",
+ title: "Urban air exposure and pediatric neuroinflammation",
+ visibility: "public",
+ citationMetadata: {
+ doi: "",
+ title: "Urban air exposure and pediatric neuroinflammation",
+ authors: ["Ada Chen", "Luis Ortega", "Mina Patel"],
+ institutions: ["North Valley University"],
+ fundingAcknowledgement: "Supported by Clean Cities Health Initiative."
+ },
+ linkedInstitutions: [
+ {
+ id: "inst-nvu",
+ name: "North Valley University",
+ role: "lead-institution",
+ verifiedDomain: "nvu.example",
+ requiresOpenAccessAcknowledgement: true
+ },
+ {
+ id: "inst-bridge",
+ name: "Bridge Children's Hospital",
+ role: "clinical-partner",
+ verifiedDomain: "bridge.example",
+ requiresOpenAccessAcknowledgement: false
+ }
+ ],
+ fundingSources: [
+ {
+ id: "grant-clean-cities",
+ funder: "Clean Cities Health Initiative",
+ grantId: "CCHI-2026-118",
+ requiredText: "Clean Cities Health Initiative",
+ requiresDoiBeforePublication: true,
+ publicAcknowledgementRequired: true
+ },
+ {
+ id: "grant-pediatric-data",
+ funder: "Pediatric Data Commons",
+ grantId: "",
+ requiredText: "Pediatric Data Commons",
+ requiresDoiBeforePublication: false,
+ publicAcknowledgementRequired: true
+ }
+ ],
+ collaborators: [
+ {
+ id: "u-ada",
+ name: "Ada Chen",
+ role: "owner",
+ institutionId: "inst-nvu",
+ emailDomain: "nvu.example",
+ orcidLinked: true,
+ publicProfile: true
+ },
+ {
+ id: "u-luis",
+ name: "Luis Ortega",
+ role: "contributor",
+ institutionId: "inst-bridge",
+ emailDomain: "bridge.example",
+ orcidLinked: false,
+ publicProfile: true
+ },
+ {
+ id: "u-mina",
+ name: "Mina Patel",
+ role: "reviewer",
+ institutionId: "inst-nvu",
+ emailDomain: "personal.example",
+ orcidLinked: true,
+ publicProfile: false
+ }
+ ],
+ auditEvents: [
+ {
+ id: "ae-1",
+ actorId: "u-ada",
+ action: "linked-funder",
+ targetId: "grant-clean-cities",
+ createdAt: "2026-05-01T12:00:00Z"
+ },
+ {
+ id: "ae-2",
+ actorId: "u-ada",
+ action: "added-institution",
+ targetId: "inst-nvu",
+ createdAt: "2026-05-02T12:00:00Z"
+ },
+ {
+ id: "ae-3",
+ actorId: "u-luis",
+ action: "updated-profile",
+ targetId: "u-luis",
+ createdAt: "2026-05-03T12:00:00Z"
+ }
+ ]
+};
+
+module.exports = {
+ project
+};
diff --git a/project-funding-attribution-guard/test.js b/project-funding-attribution-guard/test.js
new file mode 100644
index 00000000..fdfe9ca7
--- /dev/null
+++ b/project-funding-attribution-guard/test.js
@@ -0,0 +1,34 @@
+"use strict";
+
+const assert = require("assert");
+const {
+ evaluateProjectFundingAttribution,
+ evaluateFunding,
+ evaluateInstitutions,
+ evaluateCitationMetadata
+} = require("./index");
+const { project } = require("./sample-data");
+
+const audit = evaluateProjectFundingAttribution(project);
+
+assert.strictEqual(audit.generatedBy, "project-funding-attribution-guard");
+assert.strictEqual(audit.projectId, project.id);
+assert.strictEqual(audit.metrics.fundingSources, 2);
+assert.ok(audit.metrics.blockedFundingSources >= 1, "fixture should expose funding blockers");
+assert.ok(audit.findings.some((finding) => finding.code === "public-project-funding-blocker"));
+assert.ok(audit.findings.some((finding) => finding.code === "affiliation-mismatch"));
+assert.ok(audit.findings.some((finding) => finding.code === "citation-metadata-warning"));
+assert.ok(audit.reviewerActions.every((action) => action.length > 20));
+
+const funding = evaluateFunding(project);
+assert.ok(funding.find((source) => source.fundingSourceId === "grant-pediatric-data").blockers.includes("grant id missing"));
+assert.ok(funding.find((source) => source.fundingSourceId === "grant-clean-cities").blockers.includes("DOI required before publication"));
+
+const institutions = evaluateInstitutions(project);
+assert.ok(institutions.find((item) => item.collaboratorId === "u-luis").blockers.includes("ORCID link missing for attributed contributor"));
+assert.ok(institutions.find((item) => item.collaboratorId === "u-mina").blockers.includes("email domain does not match institution domain"));
+
+const citation = evaluateCitationMetadata(project);
+assert.strictEqual(citation.readiness, "ready-with-warnings");
+
+console.log("project-funding-attribution-guard tests passed");