diff --git a/README.md b/README.md
index d338cf6..70155c4 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,6 @@
# deepevents.ai
deepevents.ai main codebase
+
+## Enterprise Tooling
+
+- `enterprise-dashboard-attribution-anomaly-monitor/` adds a self-contained #19 slice for admin dashboard attribution quality, service-account separation, visibility-boundary checks, and collaboration-inflation review.
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/README.md b/enterprise-dashboard-attribution-anomaly-monitor/README.md
new file mode 100644
index 0000000..38193c0
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/README.md
@@ -0,0 +1,33 @@
+# Enterprise Dashboard Attribution Anomaly Monitor
+
+This module is a focused Enterprise Tooling slice for SCIBASE issue #19. It protects organization-wide admin dashboards from distorted productivity, collaboration, and ROI metrics before institutional leaders use those metrics for compliance or funding decisions.
+
+## What It Adds
+
+- Canonical researcher attribution across linked identities such as ORCID and GitHub accounts.
+- Service-account activity separation so automation does not inflate researcher productivity.
+- Private-project visibility boundary checks for institution-level admins.
+- Usage-spike, duplicate-identity, service-credit, private-boundary, and cross-lab collaboration-inflation anomaly detection.
+- Researcher and lab credit summaries for executive dashboards.
+- Reviewer packets, JSON output, and SVG preview for admin review.
+
+## Why This Is Distinct
+
+Existing #19 submissions cover exports, webhooks, compliance evidence, identity provisioning, retention, data residency, SLA, lab inventory, secret rotation, quotas, API changes, connector certification, incident response, funder reporting, and AI model governance. This slice focuses specifically on admin dashboard attribution quality and metric integrity.
+
+## Run
+
+```bash
+node enterprise-dashboard-attribution-anomaly-monitor/test.js
+node enterprise-dashboard-attribution-anomaly-monitor/demo.js
+```
+
+The demo writes:
+
+- `enterprise-dashboard-attribution-anomaly-monitor/dashboard-attribution-report.json`
+- `enterprise-dashboard-attribution-anomaly-monitor/reviewer-packet.md`
+- `enterprise-dashboard-attribution-anomaly-monitor/demo.svg`
+
+## Decision Policy
+
+High-severity attribution anomalies hold the executive dashboard until resolved. Medium anomalies remain visible only with dashboard annotations and owner confirmation requests.
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/dashboard-attribution-report.json b/enterprise-dashboard-attribution-anomaly-monitor/dashboard-attribution-report.json
new file mode 100644
index 0000000..d85d05c
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/dashboard-attribution-report.json
@@ -0,0 +1,290 @@
+{
+ "generatedAt": "2026-05-20T11:02:55.616Z",
+ "credits": [
+ {
+ "eventId": "evt:001",
+ "projectId": "project:neuro-alpha",
+ "actorId": "user:amy-orcid",
+ "canonicalResearcherId": "researcher:amy-chen",
+ "type": "dataset_upload",
+ "timestamp": "2026-05-20T02:00:00Z",
+ "rawCredit": 16.8,
+ "dashboardCredit": 16.8,
+ "serviceAccount": false,
+ "duplicateAlias": true,
+ "privateBoundary": false,
+ "labId": "lab:neuro",
+ "collaborationPair": [
+ "lab:neuro",
+ "lab:materials"
+ ]
+ },
+ {
+ "eventId": "evt:002",
+ "projectId": "project:neuro-alpha",
+ "actorId": "user:amy-github",
+ "canonicalResearcherId": "researcher:amy-chen",
+ "type": "code_commit",
+ "timestamp": "2026-05-20T02:15:00Z",
+ "rawCredit": 13.2,
+ "dashboardCredit": 13.2,
+ "serviceAccount": false,
+ "duplicateAlias": true,
+ "privateBoundary": false,
+ "labId": "lab:neuro",
+ "collaborationPair": [
+ "lab:neuro",
+ "lab:materials"
+ ]
+ },
+ {
+ "eventId": "evt:003",
+ "projectId": "project:neuro-alpha",
+ "actorId": "user:sync-bot",
+ "canonicalResearcherId": "user:sync-bot",
+ "type": "ai_review_generated",
+ "timestamp": "2026-05-20T02:30:00Z",
+ "rawCredit": 30,
+ "dashboardCredit": 0,
+ "serviceAccount": true,
+ "duplicateAlias": false,
+ "privateBoundary": false,
+ "labId": "lab:neuro",
+ "collaborationPair": [
+ "lab:neuro",
+ "lab:materials"
+ ]
+ },
+ {
+ "eventId": "evt:004",
+ "projectId": "project:materials-sensor",
+ "actorId": "user:sync-bot",
+ "canonicalResearcherId": "user:sync-bot",
+ "type": "repository_export",
+ "timestamp": "2026-05-20T02:45:00Z",
+ "rawCredit": 36,
+ "dashboardCredit": 0,
+ "serviceAccount": true,
+ "duplicateAlias": false,
+ "privateBoundary": false,
+ "labId": "lab:materials",
+ "collaborationPair": [
+ "lab:neuro",
+ "lab:materials"
+ ]
+ },
+ {
+ "eventId": "evt:005",
+ "projectId": "project:materials-sensor",
+ "actorId": "user:ben",
+ "canonicalResearcherId": "user:ben",
+ "type": "peer_review_completed",
+ "timestamp": "2026-05-20T03:00:00Z",
+ "rawCredit": 40,
+ "dashboardCredit": 40,
+ "serviceAccount": false,
+ "duplicateAlias": false,
+ "privateBoundary": false,
+ "labId": "lab:materials",
+ "collaborationPair": [
+ "lab:neuro",
+ "lab:materials"
+ ]
+ },
+ {
+ "eventId": "evt:006",
+ "projectId": "project:neuro-alpha",
+ "actorId": "user:amy-orcid",
+ "canonicalResearcherId": "researcher:amy-chen",
+ "type": "manuscript_edit",
+ "timestamp": "2026-05-20T03:05:00Z",
+ "rawCredit": 32,
+ "dashboardCredit": 32,
+ "serviceAccount": false,
+ "duplicateAlias": true,
+ "privateBoundary": false,
+ "labId": "lab:neuro",
+ "collaborationPair": [
+ "lab:neuro",
+ "lab:materials"
+ ]
+ },
+ {
+ "eventId": "evt:007",
+ "projectId": "project:neuro-alpha",
+ "actorId": "user:amy-github",
+ "canonicalResearcherId": "researcher:amy-chen",
+ "type": "dataset_upload",
+ "timestamp": "2026-05-20T03:20:00Z",
+ "rawCredit": 49,
+ "dashboardCredit": 49,
+ "serviceAccount": false,
+ "duplicateAlias": true,
+ "privateBoundary": false,
+ "labId": "lab:neuro",
+ "collaborationPair": [
+ "lab:neuro",
+ "lab:materials"
+ ]
+ },
+ {
+ "eventId": "evt:008",
+ "projectId": "project:external-secret",
+ "actorId": "user:external",
+ "canonicalResearcherId": "user:external",
+ "type": "repository_export",
+ "timestamp": "2026-05-20T03:40:00Z",
+ "rawCredit": 18,
+ "dashboardCredit": 18,
+ "serviceAccount": false,
+ "duplicateAlias": false,
+ "privateBoundary": true,
+ "labId": "lab:external",
+ "collaborationPair": null
+ }
+ ],
+ "metrics": {
+ "researcherCredits": [
+ {
+ "researcherId": "researcher:amy-chen",
+ "credit": 111
+ },
+ {
+ "researcherId": "user:ben",
+ "credit": 40
+ },
+ {
+ "researcherId": "user:sync-bot",
+ "credit": 0
+ }
+ ],
+ "labCredits": [
+ {
+ "labId": "lab:neuro",
+ "credit": 111
+ },
+ {
+ "labId": "lab:materials",
+ "credit": 40
+ }
+ ]
+ },
+ "anomalies": [
+ {
+ "kind": "usage_spike",
+ "key": "researcher:amy-chen:2026-05-20",
+ "severity": "high",
+ "score": 111,
+ "reason": "dashboard credit 111 in one day exceeds institutional review threshold"
+ },
+ {
+ "kind": "service_account_credit",
+ "severity": "high",
+ "score": 36,
+ "reason": "service account activity must not count as researcher productivity",
+ "eventIds": [
+ "evt:004"
+ ],
+ "actorId": "user:sync-bot"
+ },
+ {
+ "kind": "service_account_credit",
+ "severity": "high",
+ "score": 30,
+ "reason": "service account activity must not count as researcher productivity",
+ "eventIds": [
+ "evt:003"
+ ],
+ "actorId": "user:sync-bot"
+ },
+ {
+ "kind": "private_project_boundary",
+ "severity": "high",
+ "score": 18,
+ "reason": "admin dashboard row references a project outside visibility boundary",
+ "eventIds": [
+ "evt:008"
+ ],
+ "projectId": "project:external-secret"
+ },
+ {
+ "kind": "duplicate_identity_credit",
+ "canonicalResearcherId": "researcher:amy-chen",
+ "severity": "medium",
+ "score": 111,
+ "reason": "multiple linked identities generated dashboard credit under one researcher",
+ "eventIds": [
+ "evt:001",
+ "evt:002",
+ "evt:006",
+ "evt:007"
+ ]
+ },
+ {
+ "kind": "collaboration_inflation",
+ "severity": "medium",
+ "score": 60,
+ "reason": "same cross-lab collaboration pair appears repeatedly in a short window",
+ "pair": [
+ "lab:materials",
+ "lab:neuro"
+ ],
+ "eventIds": [
+ "evt:001",
+ "evt:002",
+ "evt:005",
+ "evt:006",
+ "evt:007"
+ ]
+ }
+ ],
+ "reviewPacket": {
+ "holdExecutiveDashboard": true,
+ "ownerActions": [
+ {
+ "kind": "usage_spike",
+ "severity": "high",
+ "action": "remove_from_executive_dashboard_until_resolved",
+ "reason": "dashboard credit 111 in one day exceeds institutional review threshold"
+ },
+ {
+ "kind": "service_account_credit",
+ "severity": "high",
+ "action": "remove_from_executive_dashboard_until_resolved",
+ "reason": "service account activity must not count as researcher productivity"
+ },
+ {
+ "kind": "service_account_credit",
+ "severity": "high",
+ "action": "remove_from_executive_dashboard_until_resolved",
+ "reason": "service account activity must not count as researcher productivity"
+ },
+ {
+ "kind": "private_project_boundary",
+ "severity": "high",
+ "action": "remove_from_executive_dashboard_until_resolved",
+ "reason": "admin dashboard row references a project outside visibility boundary"
+ },
+ {
+ "kind": "duplicate_identity_credit",
+ "severity": "medium",
+ "action": "annotate_dashboard_metric_and_request_owner_confirmation",
+ "reason": "multiple linked identities generated dashboard credit under one researcher"
+ },
+ {
+ "kind": "collaboration_inflation",
+ "severity": "medium",
+ "action": "annotate_dashboard_metric_and_request_owner_confirmation",
+ "reason": "same cross-lab collaboration pair appears repeatedly in a short window"
+ }
+ ]
+ },
+ "stats": {
+ "eventCount": 8,
+ "creditedResearcherCount": 2,
+ "anomalyCount": 6,
+ "highSeverityCount": 4,
+ "serviceAccountEvents": 2,
+ "hiddenBoundaryEvents": 1
+ }
+}
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/demo.js b/enterprise-dashboard-attribution-anomaly-monitor/demo.js
new file mode 100644
index 0000000..489942e
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/demo.js
@@ -0,0 +1,19 @@
+"use strict";
+
+const fs = require("node:fs");
+const path = require("node:path");
+const {
+ buildDashboardAttributionMonitor,
+ buildReviewerMarkdown,
+ renderDashboardSvg
+} = require("./index");
+const sampleData = require("./sample-data");
+
+const report = buildDashboardAttributionMonitor(sampleData);
+const outDir = __dirname;
+
+fs.writeFileSync(path.join(outDir, "dashboard-attribution-report.json"), `${JSON.stringify(report, null, 2)}\n`);
+fs.writeFileSync(path.join(outDir, "reviewer-packet.md"), buildReviewerMarkdown(report));
+fs.writeFileSync(path.join(outDir, "demo.svg"), renderDashboardSvg(report));
+
+console.log(JSON.stringify(report, null, 2));
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/demo.mp4 b/enterprise-dashboard-attribution-anomaly-monitor/demo.mp4
new file mode 100644
index 0000000..1272bab
Binary files /dev/null and b/enterprise-dashboard-attribution-anomaly-monitor/demo.mp4 differ
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/demo.svg b/enterprise-dashboard-attribution-anomaly-monitor/demo.svg
new file mode 100644
index 0000000..d8a67f3
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/demo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/index.js b/enterprise-dashboard-attribution-anomaly-monitor/index.js
new file mode 100644
index 0000000..542d8b7
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/index.js
@@ -0,0 +1,290 @@
+"use strict";
+
+const EVENT_WEIGHTS = Object.freeze({
+ manuscript_edit: 8,
+ dataset_upload: 14,
+ code_commit: 12,
+ ai_review_generated: 10,
+ peer_review_completed: 16,
+ repository_export: 18,
+ login: 4,
+ service_sync: 2
+});
+
+function assertArray(value, name) {
+ if (!Array.isArray(value)) throw new TypeError(`${name} must be an array`);
+}
+
+function round(value, digits = 2) {
+ const factor = 10 ** digits;
+ return Math.round(value * factor) / factor;
+}
+
+function hoursBetween(left, right) {
+ return Math.abs(new Date(right).getTime() - new Date(left).getTime()) / 36e5;
+}
+
+function indexById(items) {
+ return new Map(items.map((item) => [item.id, item]));
+}
+
+function visibleToAdmin(project, admin) {
+ if (project.visibility === "public") return true;
+ if (project.institutionId !== admin.institutionId) return false;
+ if (project.visibility === "institutional-only") return true;
+ return (admin.allowedPrivateProjectIds || []).includes(project.id);
+}
+
+function canonicalResearcherId(actor, identityLinks) {
+ if (!actor) return null;
+ const link = identityLinks.find((item) => item.userIds.includes(actor.id));
+ return link?.canonicalId || actor.id;
+}
+
+function eventCredit(event, context) {
+ const actor = context.users.get(event.actorId);
+ const project = context.projects.get(event.projectId);
+ const canonicalId = canonicalResearcherId(actor, context.identityLinks);
+ const base = EVENT_WEIGHTS[event.type] || 5;
+ const serviceAccount = actor?.type === "service";
+ const privateBoundary = !visibleToAdmin(project, context.admin);
+ const duplicateAlias = actor?.id !== canonicalId;
+ const score = serviceAccount ? 0 : base * Number(event.creditMultiplier ?? 1);
+
+ return {
+ eventId: event.id,
+ projectId: event.projectId,
+ actorId: event.actorId,
+ canonicalResearcherId: canonicalId,
+ type: event.type,
+ timestamp: event.timestamp,
+ rawCredit: round(base * Number(event.creditMultiplier ?? 1)),
+ dashboardCredit: round(score),
+ serviceAccount,
+ duplicateAlias,
+ privateBoundary,
+ labId: project?.labId,
+ collaborationPair: event.collaborationPair || null
+ };
+}
+
+function detectUsageSpike(credits) {
+ const byActorDay = new Map();
+ for (const credit of credits) {
+ const day = credit.timestamp.slice(0, 10);
+ const key = `${credit.canonicalResearcherId}:${day}`;
+ byActorDay.set(key, (byActorDay.get(key) || 0) + credit.dashboardCredit);
+ }
+ return [...byActorDay.entries()]
+ .filter(([, value]) => value >= 70)
+ .map(([key, value]) => ({
+ kind: "usage_spike",
+ key,
+ severity: value >= 100 ? "high" : "medium",
+ score: round(value),
+ reason: `dashboard credit ${round(value)} in one day exceeds institutional review threshold`
+ }));
+}
+
+function detectDuplicateInflation(credits) {
+ const aliasEvents = credits.filter((credit) => credit.duplicateAlias && credit.dashboardCredit > 0);
+ const byCanonical = new Map();
+ for (const credit of aliasEvents) {
+ if (!byCanonical.has(credit.canonicalResearcherId)) byCanonical.set(credit.canonicalResearcherId, []);
+ byCanonical.get(credit.canonicalResearcherId).push(credit);
+ }
+ return [...byCanonical.entries()]
+ .filter(([, items]) => new Set(items.map((item) => item.actorId)).size > 1)
+ .map(([canonicalResearcherId, items]) => ({
+ kind: "duplicate_identity_credit",
+ canonicalResearcherId,
+ severity: "medium",
+ score: round(items.reduce((sum, item) => sum + item.dashboardCredit, 0)),
+ reason: "multiple linked identities generated dashboard credit under one researcher",
+ eventIds: items.map((item) => item.eventId)
+ }));
+}
+
+function detectServiceAccountCredit(credits) {
+ const serviceCredits = credits.filter((credit) => credit.serviceAccount && credit.rawCredit > 0);
+ return serviceCredits.map((credit) => ({
+ kind: "service_account_credit",
+ severity: "high",
+ score: credit.rawCredit,
+ reason: "service account activity must not count as researcher productivity",
+ eventIds: [credit.eventId],
+ actorId: credit.actorId
+ }));
+}
+
+function detectPrivateBoundaryLeak(credits) {
+ return credits
+ .filter((credit) => credit.privateBoundary)
+ .map((credit) => ({
+ kind: "private_project_boundary",
+ severity: "high",
+ score: credit.rawCredit,
+ reason: "admin dashboard row references a project outside visibility boundary",
+ eventIds: [credit.eventId],
+ projectId: credit.projectId
+ }));
+}
+
+function detectCollaborationInflation(credits) {
+ const pairs = new Map();
+ for (const credit of credits) {
+ if (!credit.collaborationPair || credit.serviceAccount) continue;
+ const key = credit.collaborationPair.slice().sort().join("::");
+ if (!pairs.has(key)) pairs.set(key, []);
+ pairs.get(key).push(credit);
+ }
+ return [...pairs.entries()]
+ .filter(([, items]) => items.length >= 4 && items.some((item, index) => index > 0 && hoursBetween(items[0].timestamp, item.timestamp) <= 3))
+ .map(([key, items]) => ({
+ kind: "collaboration_inflation",
+ severity: "medium",
+ score: items.length * 12,
+ reason: "same cross-lab collaboration pair appears repeatedly in a short window",
+ pair: key.split("::"),
+ eventIds: items.map((item) => item.eventId)
+ }));
+}
+
+function buildMetrics(credits) {
+ const byResearcher = new Map();
+ const byLab = new Map();
+ for (const credit of credits) {
+ if (credit.privateBoundary) continue;
+ byResearcher.set(
+ credit.canonicalResearcherId,
+ round((byResearcher.get(credit.canonicalResearcherId) || 0) + credit.dashboardCredit)
+ );
+ if (credit.labId) byLab.set(credit.labId, round((byLab.get(credit.labId) || 0) + credit.dashboardCredit));
+ }
+ return {
+ researcherCredits: [...byResearcher.entries()]
+ .map(([researcherId, credit]) => ({ researcherId, credit }))
+ .sort((a, b) => b.credit - a.credit),
+ labCredits: [...byLab.entries()]
+ .map(([labId, credit]) => ({ labId, credit }))
+ .sort((a, b) => b.credit - a.credit)
+ };
+}
+
+function buildDashboardAttributionMonitor(input) {
+ const data = input || {};
+ assertArray(data.users, "users");
+ assertArray(data.projects, "projects");
+ assertArray(data.events, "events");
+ const context = {
+ users: indexById(data.users),
+ projects: indexById(data.projects),
+ identityLinks: data.identityLinks || [],
+ admin: data.admin || {}
+ };
+ const credits = data.events.map((event) => eventCredit(event, context));
+ const anomalies = [
+ ...detectUsageSpike(credits),
+ ...detectDuplicateInflation(credits),
+ ...detectServiceAccountCredit(credits),
+ ...detectPrivateBoundaryLeak(credits),
+ ...detectCollaborationInflation(credits)
+ ].sort((a, b) => {
+ const rank = { high: 3, medium: 2, low: 1 };
+ return (rank[b.severity] || 0) - (rank[a.severity] || 0) || b.score - a.score;
+ });
+ const metrics = buildMetrics(credits);
+ const reviewPacket = {
+ holdExecutiveDashboard: anomalies.some((item) => item.severity === "high"),
+ ownerActions: anomalies.map((item) => ({
+ kind: item.kind,
+ severity: item.severity,
+ action:
+ item.severity === "high"
+ ? "remove_from_executive_dashboard_until_resolved"
+ : "annotate_dashboard_metric_and_request_owner_confirmation",
+ reason: item.reason
+ }))
+ };
+
+ return {
+ generatedAt: new Date().toISOString(),
+ credits,
+ metrics,
+ anomalies,
+ reviewPacket,
+ stats: {
+ eventCount: data.events.length,
+ creditedResearcherCount: metrics.researcherCredits.filter((item) => item.credit > 0).length,
+ anomalyCount: anomalies.length,
+ highSeverityCount: anomalies.filter((item) => item.severity === "high").length,
+ serviceAccountEvents: credits.filter((item) => item.serviceAccount).length,
+ hiddenBoundaryEvents: credits.filter((item) => item.privateBoundary).length
+ }
+ };
+}
+
+function buildReviewerMarkdown(report) {
+ const lines = ["# Enterprise Dashboard Attribution Review", ""];
+ lines.push(`Events reviewed: ${report.stats.eventCount}`);
+ lines.push(`Anomalies: ${report.stats.anomalyCount}; high severity: ${report.stats.highSeverityCount}`);
+ lines.push(`Executive dashboard hold: ${report.reviewPacket.holdExecutiveDashboard ? "yes" : "no"}`);
+ lines.push("");
+ lines.push("## Top Researcher Credits");
+ for (const item of report.metrics.researcherCredits.slice(0, 6)) {
+ lines.push(`- ${item.researcherId}: ${item.credit}`);
+ }
+ lines.push("");
+ lines.push("## Anomalies");
+ for (const item of report.anomalies) {
+ lines.push(`- ${item.severity.toUpperCase()} ${item.kind}: ${item.reason}`);
+ }
+ return `${lines.join("\n")}\n`;
+}
+
+function renderDashboardSvg(report) {
+ const width = 940;
+ const rowHeight = 78;
+ const rows = report.anomalies.slice(0, 6);
+ const height = 128 + rows.length * rowHeight;
+ const body = rows
+ .map((item, index) => {
+ const y = 88 + index * rowHeight;
+ const color = item.severity === "high" ? "#be123c" : "#b45309";
+ return [
+ ``,
+ ``,
+ `${escapeXml(item.kind)}`,
+ `${escapeXml(item.reason)}`,
+ ``,
+ `${escapeXml(item.severity)}`,
+ ``
+ ].join("");
+ })
+ .join("");
+ return [
+ ``
+ ].join("");
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+module.exports = {
+ EVENT_WEIGHTS,
+ buildDashboardAttributionMonitor,
+ buildReviewerMarkdown,
+ eventCredit,
+ renderDashboardSvg,
+ visibleToAdmin
+};
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/requirements-map.md b/enterprise-dashboard-attribution-anomaly-monitor/requirements-map.md
new file mode 100644
index 0000000..a304035
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/requirements-map.md
@@ -0,0 +1,21 @@
+# Requirements Map
+
+## Issue #19: Enterprise Tooling
+
+| Requirement | Coverage |
+| --- | --- |
+| Organization-wide admin dashboards | `buildDashboardAttributionMonitor` produces admin-facing metrics and review packets. |
+| Contributor analytics and top researchers | `buildMetrics` rolls events into canonical researcher credits. |
+| Cross-lab collaborations | `detectCollaborationInflation` flags repeated collaboration-pair inflation. |
+| Usage stats and productivity metrics | Event weights cover manuscript edits, dataset uploads, code commits, AI reviews, peer reviews, exports, and logins. |
+| Compliance tracking and visibility boundaries | `visibleToAdmin` and private-boundary anomalies keep restricted/private projects out of unauthorized dashboards. |
+| ORCID/HRIS-style identity alignment | `canonicalResearcherId` uses identity links to de-duplicate ORCID/GitHub-style aliases. |
+| Automation separation | `detectServiceAccountCredit` prevents service accounts from inflating researcher credit. |
+| Tests and demo | `test.js` covers duplicate identity credit, service account credit removal, private-boundary leaks, collaboration inflation, usage spikes, reviewer Markdown, and visibility checks. `demo.js` emits JSON, Markdown, and SVG artifacts. |
+
+## Acceptance Notes
+
+- Synthetic data only; no external service calls or private data.
+- No dependencies beyond the Node.js standard library.
+- This is dashboard metric integrity, not another export, webhook, identity provisioning, quota, incident, or AI governance module.
+- High-severity anomalies hold executive dashboards rather than silently adjusting numbers.
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/reviewer-packet.md b/enterprise-dashboard-attribution-anomaly-monitor/reviewer-packet.md
new file mode 100644
index 0000000..370e08f
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/reviewer-packet.md
@@ -0,0 +1,18 @@
+# Enterprise Dashboard Attribution Review
+
+Events reviewed: 8
+Anomalies: 6; high severity: 4
+Executive dashboard hold: yes
+
+## Top Researcher Credits
+- researcher:amy-chen: 111
+- user:ben: 40
+- user:sync-bot: 0
+
+## Anomalies
+- HIGH usage_spike: dashboard credit 111 in one day exceeds institutional review threshold
+- HIGH service_account_credit: service account activity must not count as researcher productivity
+- HIGH service_account_credit: service account activity must not count as researcher productivity
+- HIGH private_project_boundary: admin dashboard row references a project outside visibility boundary
+- MEDIUM duplicate_identity_credit: multiple linked identities generated dashboard credit under one researcher
+- MEDIUM collaboration_inflation: same cross-lab collaboration pair appears repeatedly in a short window
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/sample-data.js b/enterprise-dashboard-attribution-anomaly-monitor/sample-data.js
new file mode 100644
index 0000000..b7415d9
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/sample-data.js
@@ -0,0 +1,151 @@
+"use strict";
+
+module.exports = {
+ admin: {
+ id: "admin:research-office",
+ institutionId: "inst:western",
+ allowedPrivateProjectIds: ["project:neuro-alpha"]
+ },
+ users: [
+ {
+ id: "user:amy-orcid",
+ name: "Amy Chen",
+ type: "researcher",
+ institutionId: "inst:western",
+ labId: "lab:neuro",
+ orcid: "0000-0001-0000-0001"
+ },
+ {
+ id: "user:amy-github",
+ name: "Amy C.",
+ type: "researcher",
+ institutionId: "inst:western",
+ labId: "lab:neuro",
+ github: "amy-research"
+ },
+ {
+ id: "user:ben",
+ name: "Ben Patel",
+ type: "researcher",
+ institutionId: "inst:western",
+ labId: "lab:materials",
+ orcid: "0000-0002-0000-0002"
+ },
+ {
+ id: "user:sync-bot",
+ name: "Repository Sync Bot",
+ type: "service",
+ institutionId: "inst:western",
+ labId: "lab:platform"
+ },
+ {
+ id: "user:external",
+ name: "External Reviewer",
+ type: "researcher",
+ institutionId: "inst:eastern",
+ labId: "lab:external"
+ }
+ ],
+ identityLinks: [
+ {
+ canonicalId: "researcher:amy-chen",
+ userIds: ["user:amy-orcid", "user:amy-github"]
+ }
+ ],
+ projects: [
+ {
+ id: "project:neuro-alpha",
+ title: "Neuro Alpha",
+ institutionId: "inst:western",
+ labId: "lab:neuro",
+ visibility: "private"
+ },
+ {
+ id: "project:materials-sensor",
+ title: "Materials Sensor",
+ institutionId: "inst:western",
+ labId: "lab:materials",
+ visibility: "institutional-only"
+ },
+ {
+ id: "project:external-secret",
+ title: "External Private Study",
+ institutionId: "inst:eastern",
+ labId: "lab:external",
+ visibility: "private"
+ }
+ ],
+ events: [
+ {
+ id: "evt:001",
+ type: "dataset_upload",
+ timestamp: "2026-05-20T02:00:00Z",
+ actorId: "user:amy-orcid",
+ projectId: "project:neuro-alpha",
+ creditMultiplier: 1.2,
+ collaborationPair: ["lab:neuro", "lab:materials"]
+ },
+ {
+ id: "evt:002",
+ type: "code_commit",
+ timestamp: "2026-05-20T02:15:00Z",
+ actorId: "user:amy-github",
+ projectId: "project:neuro-alpha",
+ creditMultiplier: 1.1,
+ collaborationPair: ["lab:neuro", "lab:materials"]
+ },
+ {
+ id: "evt:003",
+ type: "ai_review_generated",
+ timestamp: "2026-05-20T02:30:00Z",
+ actorId: "user:sync-bot",
+ projectId: "project:neuro-alpha",
+ creditMultiplier: 3,
+ collaborationPair: ["lab:neuro", "lab:materials"]
+ },
+ {
+ id: "evt:004",
+ type: "repository_export",
+ timestamp: "2026-05-20T02:45:00Z",
+ actorId: "user:sync-bot",
+ projectId: "project:materials-sensor",
+ creditMultiplier: 2,
+ collaborationPair: ["lab:neuro", "lab:materials"]
+ },
+ {
+ id: "evt:005",
+ type: "peer_review_completed",
+ timestamp: "2026-05-20T03:00:00Z",
+ actorId: "user:ben",
+ projectId: "project:materials-sensor",
+ creditMultiplier: 2.5,
+ collaborationPair: ["lab:neuro", "lab:materials"]
+ },
+ {
+ id: "evt:006",
+ type: "manuscript_edit",
+ timestamp: "2026-05-20T03:05:00Z",
+ actorId: "user:amy-orcid",
+ projectId: "project:neuro-alpha",
+ creditMultiplier: 4,
+ collaborationPair: ["lab:neuro", "lab:materials"]
+ },
+ {
+ id: "evt:007",
+ type: "dataset_upload",
+ timestamp: "2026-05-20T03:20:00Z",
+ actorId: "user:amy-github",
+ projectId: "project:neuro-alpha",
+ creditMultiplier: 3.5,
+ collaborationPair: ["lab:neuro", "lab:materials"]
+ },
+ {
+ id: "evt:008",
+ type: "repository_export",
+ timestamp: "2026-05-20T03:40:00Z",
+ actorId: "user:external",
+ projectId: "project:external-secret",
+ creditMultiplier: 1
+ }
+ ]
+};
diff --git a/enterprise-dashboard-attribution-anomaly-monitor/test.js b/enterprise-dashboard-attribution-anomaly-monitor/test.js
new file mode 100644
index 0000000..de10b0a
--- /dev/null
+++ b/enterprise-dashboard-attribution-anomaly-monitor/test.js
@@ -0,0 +1,58 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const {
+ buildDashboardAttributionMonitor,
+ buildReviewerMarkdown,
+ eventCredit,
+ visibleToAdmin
+} = require("./index");
+const sampleData = require("./sample-data");
+
+const report = buildDashboardAttributionMonitor(sampleData);
+
+assert.equal(report.stats.eventCount, 8);
+assert.ok(report.stats.anomalyCount >= 5);
+assert.ok(report.stats.highSeverityCount >= 2);
+assert.equal(report.stats.serviceAccountEvents, 2);
+assert.equal(report.stats.hiddenBoundaryEvents, 1);
+assert.equal(report.reviewPacket.holdExecutiveDashboard, true);
+
+const kinds = new Set(report.anomalies.map((item) => item.kind));
+assert.ok(kinds.has("service_account_credit"));
+assert.ok(kinds.has("private_project_boundary"));
+assert.ok(kinds.has("duplicate_identity_credit"));
+assert.ok(kinds.has("collaboration_inflation"));
+assert.ok(kinds.has("usage_spike"));
+
+const amy = report.metrics.researcherCredits.find((item) => item.researcherId === "researcher:amy-chen");
+assert.ok(amy.credit > 100);
+
+const serviceCredit = report.credits.find((item) => item.actorId === "user:sync-bot");
+assert.equal(serviceCredit.dashboardCredit, 0);
+assert.equal(serviceCredit.serviceAccount, true);
+
+const hiddenCredit = report.credits.find((item) => item.eventId === "evt:008");
+assert.equal(hiddenCredit.privateBoundary, true);
+
+const users = new Map(sampleData.users.map((user) => [user.id, user]));
+const projects = new Map(sampleData.projects.map((project) => [project.id, project]));
+const admin = sampleData.admin;
+assert.equal(visibleToAdmin(projects.get("project:neuro-alpha"), admin), true);
+assert.equal(visibleToAdmin(projects.get("project:external-secret"), admin), false);
+
+const credit = eventCredit(sampleData.events[1], {
+ users,
+ projects,
+ identityLinks: sampleData.identityLinks,
+ admin
+});
+assert.equal(credit.canonicalResearcherId, "researcher:amy-chen");
+assert.equal(credit.duplicateAlias, true);
+
+const markdown = buildReviewerMarkdown(report);
+assert.ok(markdown.includes("Enterprise Dashboard Attribution Review"));
+assert.ok(markdown.includes("Executive dashboard hold: yes"));
+assert.ok(markdown.includes("service_account_credit"));
+
+console.log("enterprise-dashboard-attribution-anomaly-monitor tests passed");