diff --git a/enterprise-initiative-tag-governance/README.md b/enterprise-initiative-tag-governance/README.md new file mode 100644 index 0000000..b78109f --- /dev/null +++ b/enterprise-initiative-tag-governance/README.md @@ -0,0 +1,32 @@ +# Enterprise Initiative Tag Governance + +Self-contained Enterprise Tooling slice for SCIBASE issue #19. + +This module validates institution-defined initiative tags such as `GRANT-TRACKED`, `DOCTORAL-WORK`, or `PUBLIC-INITIATIVE` before they appear in admin dashboards, analytics rollups, or outbound webhook events. + +## What It Covers + +- Controlled initiative tag vocabularies for institutional admins. +- Scope checks by department, lab, or organization. +- Owner approval, expiry, funder linkage, required evidence, and reproducibility thresholds. +- Private or restricted project boundary checks before dashboard publishing. +- Mutual exclusion checks for conflicting internal and public tags. +- Dashboard rollups by tag and department. +- Signed webhook-ready governance events and a deterministic audit digest. + +## Local Validation + +```bash +node enterprise-initiative-tag-governance/test.js +node enterprise-initiative-tag-governance/demo.js +node --check enterprise-initiative-tag-governance/index.js +node --check enterprise-initiative-tag-governance/test.js +node --check enterprise-initiative-tag-governance/demo.js +ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 enterprise-initiative-tag-governance/demo.mp4 +``` + +The demo writes: + +- `enterprise-initiative-tag-governance/reports/reviewer-packet.json` +- `enterprise-initiative-tag-governance/demo.svg` +- `enterprise-initiative-tag-governance/demo.mp4` is included as the short demo artifact. diff --git a/enterprise-initiative-tag-governance/acceptance-notes.md b/enterprise-initiative-tag-governance/acceptance-notes.md new file mode 100644 index 0000000..f94059d --- /dev/null +++ b/enterprise-initiative-tag-governance/acceptance-notes.md @@ -0,0 +1,29 @@ +# Acceptance Notes + +## Reviewer Checklist + +- Confirm `assessEnterpriseInitiativeTags` accepts synthetic policies, projects, assignments, and evidence records. +- Confirm approved tags can appear in dashboard rollups and signed webhook events. +- Confirm restricted or expired tags are held before dashboard publication. +- Confirm mutually exclusive tags on the same project are blocked. +- Confirm funder, open-access, and reproducibility requirements are surfaced as blockers or warnings. + +## Validation Evidence + +Run locally from the repository root: + +```bash +node enterprise-initiative-tag-governance/test.js +node enterprise-initiative-tag-governance/demo.js +node --check enterprise-initiative-tag-governance/index.js +node --check enterprise-initiative-tag-governance/test.js +node --check enterprise-initiative-tag-governance/demo.js +ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 enterprise-initiative-tag-governance/demo.mp4 +``` + +Expected output includes: + +- `enterprise-initiative-tag-governance tests passed` +- A reviewer packet at `enterprise-initiative-tag-governance/reports/reviewer-packet.json` +- A static demo frame at `enterprise-initiative-tag-governance/demo.svg` +- A 1280x720 H.264 MP4 demo at `enterprise-initiative-tag-governance/demo.mp4` diff --git a/enterprise-initiative-tag-governance/demo.js b/enterprise-initiative-tag-governance/demo.js new file mode 100644 index 0000000..96e64cf --- /dev/null +++ b/enterprise-initiative-tag-governance/demo.js @@ -0,0 +1,124 @@ +"use strict" + +const fs = require("node:fs") +const path = require("node:path") +const { assessEnterpriseInitiativeTags } = require("./index") + +const input = { + now: "2026-05-20T00:00:00.000Z", + organizationId: "research-office-demo", + policies: [ + { + id: "GRANT-TRACKED", + label: "Grant tracked", + allowedScopes: ["biology", "materials"], + requiresOwnerApproval: true, + requiresExpiry: true, + maxDurationDays: 540, + requiredEvidence: ["grant-award-letter"], + publishToDashboard: true, + funderId: "nih-r01-42", + minimumReproducibilityScore: 80, + openAccessRequired: true, + }, + { + id: "DOCTORAL-WORK", + label: "Doctoral work", + allowedScopes: ["biology", "physics"], + requiresOwnerApproval: true, + requiresExpiry: true, + publishToDashboard: true, + }, + { + id: "PUBLIC-INITIATIVE", + label: "Public initiative", + allowedScopes: ["organization"], + requiresOwnerApproval: true, + requiresExpiry: true, + publishToDashboard: true, + restrictedDataAllowed: false, + }, + ], + evidence: [{ id: "grant-award-letter" }], + projects: [ + { + id: "project-atlas", + title: "Single-cell atlas release", + department: "biology", + visibility: "public", + dataClassification: "open", + funderIds: ["nih-r01-42"], + compliance: { openAccessStatus: "open", reproducibilityScore: 92 }, + }, + { + id: "project-embargo", + title: "Embargoed sponsor validation", + department: "oncology", + visibility: "private", + dataClassification: "restricted", + funderIds: ["sponsor-7"], + compliance: { openAccessStatus: "embargoed", reproducibilityScore: 68 }, + }, + ], + assignments: [ + { + projectId: "project-atlas", + tagId: "GRANT-TRACKED", + scope: "biology", + assignedAt: "2026-01-10T00:00:00.000Z", + expiresAt: "2026-12-31T00:00:00.000Z", + ownerApproval: true, + evidenceIds: ["grant-award-letter"], + }, + { + projectId: "project-atlas", + tagId: "DOCTORAL-WORK", + scope: "biology", + assignedAt: "2026-03-01T00:00:00.000Z", + expiresAt: "2026-11-30T00:00:00.000Z", + ownerApproval: true, + }, + { + projectId: "project-embargo", + tagId: "PUBLIC-INITIATIVE", + scope: "organization", + assignedAt: "2025-01-01T00:00:00.000Z", + expiresAt: "2026-01-01T00:00:00.000Z", + ownerApproval: false, + }, + ], +} + +const result = assessEnterpriseInitiativeTags(input) +const outDir = path.join(__dirname, "reports") +fs.mkdirSync(outDir, { recursive: true }) +fs.writeFileSync(path.join(outDir, "reviewer-packet.json"), `${JSON.stringify(result, null, 2)}\n`) + +const svg = ` + + + + Enterprise Initiative Tag Governance + Admin dashboard custom tags checked before analytics rollups and webhooks. + + ${result.summary.approved} + approved tags + + ${result.summary.warningCount} + warnings + + ${result.summary.held} + held tags + Reviewer signal + Status: ${result.status} + Webhook events: ${result.webhookEvents.length} + Audit digest: ${result.auditDigest.slice(0, 24)}... + +` + +fs.writeFileSync(path.join(__dirname, "demo.svg"), svg) +console.log(`status=${result.status}`) +console.log(`approved=${result.summary.approved} held=${result.summary.held} warnings=${result.summary.warningCount}`) +console.log(`auditDigest=${result.auditDigest}`) +console.log(`wrote ${path.relative(process.cwd(), path.join(outDir, "reviewer-packet.json"))}`) +console.log(`wrote ${path.relative(process.cwd(), path.join(__dirname, "demo.svg"))}`) diff --git a/enterprise-initiative-tag-governance/demo.mp4 b/enterprise-initiative-tag-governance/demo.mp4 new file mode 100644 index 0000000..6e30a81 Binary files /dev/null and b/enterprise-initiative-tag-governance/demo.mp4 differ diff --git a/enterprise-initiative-tag-governance/demo.svg b/enterprise-initiative-tag-governance/demo.svg new file mode 100644 index 0000000..2e0b12a --- /dev/null +++ b/enterprise-initiative-tag-governance/demo.svg @@ -0,0 +1,20 @@ + + + + + Enterprise Initiative Tag Governance + Admin dashboard custom tags checked before analytics rollups and webhooks. + + 2 + approved tags + + 0 + warnings + + 1 + held tags + Reviewer signal + Status: blocked + Webhook events: 3 + Audit digest: ce54335865ff8107bef5be05... + diff --git a/enterprise-initiative-tag-governance/index.js b/enterprise-initiative-tag-governance/index.js new file mode 100644 index 0000000..0d8b520 --- /dev/null +++ b/enterprise-initiative-tag-governance/index.js @@ -0,0 +1,319 @@ +"use strict" + +const crypto = require("node:crypto") + +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 normalizeId(value) { + return String(value || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") +} + +function parseDate(value) { + if (!value) return null + const parsed = new Date(value) + return Number.isNaN(parsed.getTime()) ? null : parsed +} + +function daysBetween(start, end) { + return Math.ceil((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)) +} + +function toArray(value) { + return Array.isArray(value) ? value : [] +} + +function makePolicyMap(policies) { + return new Map( + toArray(policies).map((policy) => { + const id = normalizeId(policy.id || policy.label) + return [ + id, + { + ...policy, + id, + label: policy.label || id, + allowedScopes: toArray(policy.allowedScopes).map(normalizeId), + mutuallyExclusiveWith: toArray(policy.mutuallyExclusiveWith).map(normalizeId), + requiredEvidence: toArray(policy.requiredEvidence), + }, + ] + }), + ) +} + +function makeProjectMap(projects) { + return new Map( + toArray(projects).map((project) => [ + String(project.id), + { + ...project, + funderIds: toArray(project.funderIds).map(String), + tags: toArray(project.tags), + }, + ]), + ) +} + +function expandAssignments(input) { + const explicit = toArray(input.assignments) + const projectTags = toArray(input.projects).flatMap((project) => + toArray(project.initiativeTags).map((tag) => ({ + ...tag, + projectId: project.id, + })), + ) + + return [...explicit, ...projectTags].map((assignment, index) => ({ + ...assignment, + index, + projectId: String(assignment.projectId || ""), + tagId: normalizeId(assignment.tagId || assignment.tag || assignment.label), + scope: normalizeId(assignment.scope || assignment.department || assignment.lab || "organization"), + evidenceIds: toArray(assignment.evidenceIds).map(String), + })) +} + +function scopeMatches(policy, project, assignment) { + if (!policy.allowedScopes.length || policy.allowedScopes.includes("*")) return true + const candidates = [ + assignment.scope, + normalizeId(project.department), + normalizeId(project.lab), + normalizeId(project.organizationId), + "organization", + ].filter(Boolean) + + return candidates.some((candidate) => policy.allowedScopes.includes(candidate)) +} + +function hasRestrictedBoundary(project) { + const visibility = normalizeId(project.visibility) + const dataClass = normalizeId(project.dataClassification) + return ["private", "restricted", "sensitive", "embargoed"].includes(visibility) || + ["private", "restricted", "sensitive", "regulated", "phi"].includes(dataClass) +} + +function findProjectConflicts(assignment, siblings, policy, policyById) { + const conflicts = [] + for (const sibling of siblings) { + if (sibling.index === assignment.index || sibling.tagId === assignment.tagId) continue + const siblingPolicy = policyById.get(sibling.tagId) + if (!siblingPolicy) continue + if (policy.mutuallyExclusiveWith.includes(sibling.tagId)) conflicts.push(sibling.tagId) + if (siblingPolicy.mutuallyExclusiveWith.includes(assignment.tagId)) conflicts.push(sibling.tagId) + } + return [...new Set(conflicts)] +} + +function evaluateAssignment(assignment, context) { + const { evidenceIds, now, policyById, projectById, assignmentsByProject } = context + const blockers = [] + const warnings = [] + const project = projectById.get(assignment.projectId) + const policy = policyById.get(assignment.tagId) + + if (!assignment.projectId) blockers.push("missing project id") + if (!assignment.tagId) blockers.push("missing initiative tag id") + if (!project) blockers.push(`unknown project ${assignment.projectId || "(blank)"}`) + if (!policy) blockers.push(`unknown initiative tag ${assignment.tagId || "(blank)"}`) + + if (project && policy) { + if (!scopeMatches(policy, project, assignment)) { + blockers.push(`tag ${policy.id} is not allowed for scope ${assignment.scope}`) + } + + if (policy.requiresOwnerApproval && !assignment.ownerApproval) { + blockers.push(`tag ${policy.id} is missing owner approval`) + } + + const missingEvidence = policy.requiredEvidence.filter((requiredId) => { + return !assignment.evidenceIds.includes(requiredId) && !evidenceIds.has(requiredId) + }) + if (missingEvidence.length > 0) { + blockers.push(`tag ${policy.id} is missing required evidence: ${missingEvidence.join(", ")}`) + } + + const assignedAt = parseDate(assignment.assignedAt) + const expiresAt = parseDate(assignment.expiresAt) + if (policy.requiresExpiry && !expiresAt) { + blockers.push(`tag ${policy.id} is missing an expiry date`) + } + if (expiresAt && expiresAt < now) { + blockers.push(`tag ${policy.id} expired on ${expiresAt.toISOString().slice(0, 10)}`) + } + if (assignedAt && expiresAt && policy.maxDurationDays && daysBetween(assignedAt, expiresAt) > policy.maxDurationDays) { + warnings.push(`tag ${policy.id} exceeds the ${policy.maxDurationDays} day maximum duration`) + } + + if (policy.publishToDashboard && hasRestrictedBoundary(project) && !policy.restrictedDataAllowed) { + blockers.push(`tag ${policy.id} would expose a restricted project in admin dashboard rollups`) + } + + if (policy.funderId && !project.funderIds.includes(String(policy.funderId))) { + blockers.push(`tag ${policy.id} requires funder ${policy.funderId}`) + } + + const score = Number(project.compliance?.reproducibilityScore ?? 0) + if (policy.minimumReproducibilityScore && score < policy.minimumReproducibilityScore) { + warnings.push(`project reproducibility score ${score} is below ${policy.minimumReproducibilityScore}`) + } + + if (policy.openAccessRequired && normalizeId(project.compliance?.openAccessStatus) !== "open") { + warnings.push(`project open access status is ${project.compliance?.openAccessStatus || "missing"}`) + } + + const conflicts = findProjectConflicts( + assignment, + assignmentsByProject.get(assignment.projectId) || [], + policy, + policyById, + ) + if (conflicts.length > 0) { + blockers.push(`tag ${policy.id} conflicts with ${conflicts.join(", ")}`) + } + } + + const status = blockers.length > 0 ? "held" : warnings.length > 0 ? "needs-review" : "approved" + const event = { + type: status === "approved" ? "enterprise_tag.approved" : "enterprise_tag.held", + projectId: assignment.projectId, + tagId: assignment.tagId, + status, + blockerCount: blockers.length, + warningCount: warnings.length, + issuedAt: now.toISOString(), + } + + return { + projectId: assignment.projectId, + tagId: assignment.tagId, + label: policy?.label || assignment.tagId, + scope: assignment.scope, + status, + blockers, + warnings, + dashboardVisible: Boolean(policy?.publishToDashboard && status !== "held"), + event: { + ...event, + signature: digest(event), + }, + } +} + +function buildDashboardRollups(assignmentReports, projectById) { + const tagRollups = new Map() + const departmentRollups = new Map() + + for (const report of assignmentReports) { + const project = projectById.get(report.projectId) + const department = project?.department || "unassigned" + if (!tagRollups.has(report.tagId)) { + tagRollups.set(report.tagId, { + tagId: report.tagId, + label: report.label, + projectCount: 0, + approvedCount: 0, + heldCount: 0, + needsReviewCount: 0, + warningCount: 0, + blockerCount: 0, + departments: new Set(), + }) + } + const tag = tagRollups.get(report.tagId) + tag.projectCount += 1 + tag.warningCount += report.warnings.length + tag.blockerCount += report.blockers.length + tag.departments.add(department) + if (report.status === "approved") tag.approvedCount += 1 + if (report.status === "held") tag.heldCount += 1 + if (report.status === "needs-review") tag.needsReviewCount += 1 + + if (!departmentRollups.has(department)) { + departmentRollups.set(department, { + department, + assignedTags: 0, + dashboardVisibleTags: 0, + heldTags: 0, + }) + } + const dept = departmentRollups.get(department) + dept.assignedTags += 1 + if (report.dashboardVisible) dept.dashboardVisibleTags += 1 + if (report.status === "held") dept.heldTags += 1 + } + + return { + byTag: [...tagRollups.values()] + .map((rollup) => ({ ...rollup, departments: [...rollup.departments].sort() })) + .sort((a, b) => a.tagId.localeCompare(b.tagId)), + byDepartment: [...departmentRollups.values()].sort((a, b) => a.department.localeCompare(b.department)), + } +} + +function assessEnterpriseInitiativeTags(input = {}) { + const now = parseDate(input.now) || new Date() + const policyById = makePolicyMap(input.policies) + const projectById = makeProjectMap(input.projects) + const evidenceIds = new Set(toArray(input.evidence).map((item) => String(item.id))) + const assignments = expandAssignments(input) + const assignmentsByProject = new Map() + + for (const assignment of assignments) { + if (!assignmentsByProject.has(assignment.projectId)) assignmentsByProject.set(assignment.projectId, []) + assignmentsByProject.get(assignment.projectId).push(assignment) + } + + const context = { evidenceIds, now, policyById, projectById, assignmentsByProject } + const assignmentReports = assignments.map((assignment) => evaluateAssignment(assignment, context)) + const blockerCount = assignmentReports.reduce((sum, report) => sum + report.blockers.length, 0) + const warningCount = assignmentReports.reduce((sum, report) => sum + report.warnings.length, 0) + const dashboardRollups = buildDashboardRollups(assignmentReports, projectById) + + const reviewerPacket = { + organizationId: input.organizationId || "enterprise-org", + evaluatedAt: now.toISOString(), + status: blockerCount > 0 ? "blocked" : warningCount > 0 ? "needs-review" : "ready", + summary: { + policies: policyById.size, + projects: projectById.size, + assignments: assignments.length, + approved: assignmentReports.filter((report) => report.status === "approved").length, + needsReview: assignmentReports.filter((report) => report.status === "needs-review").length, + held: assignmentReports.filter((report) => report.status === "held").length, + blockerCount, + warningCount, + }, + assignmentReports, + dashboardRollups, + webhookEvents: assignmentReports.map((report) => report.event), + } + + return { + ...reviewerPacket, + auditDigest: digest(reviewerPacket), + } +} + +module.exports = { + assessEnterpriseInitiativeTags, + normalizeId, + stableStringify, +} diff --git a/enterprise-initiative-tag-governance/reports/reviewer-packet.json b/enterprise-initiative-tag-governance/reports/reviewer-packet.json new file mode 100644 index 0000000..a42ef42 --- /dev/null +++ b/enterprise-initiative-tag-governance/reports/reviewer-packet.json @@ -0,0 +1,171 @@ +{ + "organizationId": "research-office-demo", + "evaluatedAt": "2026-05-20T00:00:00.000Z", + "status": "blocked", + "summary": { + "policies": 3, + "projects": 2, + "assignments": 3, + "approved": 2, + "needsReview": 0, + "held": 1, + "blockerCount": 3, + "warningCount": 0 + }, + "assignmentReports": [ + { + "projectId": "project-atlas", + "tagId": "grant-tracked", + "label": "Grant tracked", + "scope": "biology", + "status": "approved", + "blockers": [], + "warnings": [], + "dashboardVisible": true, + "event": { + "type": "enterprise_tag.approved", + "projectId": "project-atlas", + "tagId": "grant-tracked", + "status": "approved", + "blockerCount": 0, + "warningCount": 0, + "issuedAt": "2026-05-20T00:00:00.000Z", + "signature": "0cd50d892226890059543852ffb28317a085118d9f2d8ba7ce29bc45d870db6e" + } + }, + { + "projectId": "project-atlas", + "tagId": "doctoral-work", + "label": "Doctoral work", + "scope": "biology", + "status": "approved", + "blockers": [], + "warnings": [], + "dashboardVisible": true, + "event": { + "type": "enterprise_tag.approved", + "projectId": "project-atlas", + "tagId": "doctoral-work", + "status": "approved", + "blockerCount": 0, + "warningCount": 0, + "issuedAt": "2026-05-20T00:00:00.000Z", + "signature": "67cf892ece6bccbfa50bc3e7f67e8d5e47923b8a7a9db135a5ffab7dbf768c37" + } + }, + { + "projectId": "project-embargo", + "tagId": "public-initiative", + "label": "Public initiative", + "scope": "organization", + "status": "held", + "blockers": [ + "tag public-initiative is missing owner approval", + "tag public-initiative expired on 2026-01-01", + "tag public-initiative would expose a restricted project in admin dashboard rollups" + ], + "warnings": [], + "dashboardVisible": false, + "event": { + "type": "enterprise_tag.held", + "projectId": "project-embargo", + "tagId": "public-initiative", + "status": "held", + "blockerCount": 3, + "warningCount": 0, + "issuedAt": "2026-05-20T00:00:00.000Z", + "signature": "ad4a314474888e8721c6931dc65e95819ab3c5044df033aa6879d31159dc29a5" + } + } + ], + "dashboardRollups": { + "byTag": [ + { + "tagId": "doctoral-work", + "label": "Doctoral work", + "projectCount": 1, + "approvedCount": 1, + "heldCount": 0, + "needsReviewCount": 0, + "warningCount": 0, + "blockerCount": 0, + "departments": [ + "biology" + ] + }, + { + "tagId": "grant-tracked", + "label": "Grant tracked", + "projectCount": 1, + "approvedCount": 1, + "heldCount": 0, + "needsReviewCount": 0, + "warningCount": 0, + "blockerCount": 0, + "departments": [ + "biology" + ] + }, + { + "tagId": "public-initiative", + "label": "Public initiative", + "projectCount": 1, + "approvedCount": 0, + "heldCount": 1, + "needsReviewCount": 0, + "warningCount": 0, + "blockerCount": 3, + "departments": [ + "oncology" + ] + } + ], + "byDepartment": [ + { + "department": "biology", + "assignedTags": 2, + "dashboardVisibleTags": 2, + "heldTags": 0 + }, + { + "department": "oncology", + "assignedTags": 1, + "dashboardVisibleTags": 0, + "heldTags": 1 + } + ] + }, + "webhookEvents": [ + { + "type": "enterprise_tag.approved", + "projectId": "project-atlas", + "tagId": "grant-tracked", + "status": "approved", + "blockerCount": 0, + "warningCount": 0, + "issuedAt": "2026-05-20T00:00:00.000Z", + "signature": "0cd50d892226890059543852ffb28317a085118d9f2d8ba7ce29bc45d870db6e" + }, + { + "type": "enterprise_tag.approved", + "projectId": "project-atlas", + "tagId": "doctoral-work", + "status": "approved", + "blockerCount": 0, + "warningCount": 0, + "issuedAt": "2026-05-20T00:00:00.000Z", + "signature": "67cf892ece6bccbfa50bc3e7f67e8d5e47923b8a7a9db135a5ffab7dbf768c37" + }, + { + "type": "enterprise_tag.held", + "projectId": "project-embargo", + "tagId": "public-initiative", + "status": "held", + "blockerCount": 3, + "warningCount": 0, + "issuedAt": "2026-05-20T00:00:00.000Z", + "signature": "ad4a314474888e8721c6931dc65e95819ab3c5044df033aa6879d31159dc29a5" + } + ], + "auditDigest": "ce54335865ff8107bef5be0524ae7bab67d38b5d234f3530b518622759f1e701" +} diff --git a/enterprise-initiative-tag-governance/requirements-map.md b/enterprise-initiative-tag-governance/requirements-map.md new file mode 100644 index 0000000..a4bd1bf --- /dev/null +++ b/enterprise-initiative-tag-governance/requirements-map.md @@ -0,0 +1,14 @@ +# Requirements Map + +| Issue #19 requirement | Coverage in this module | +| --- | --- | +| Admin dashboards | Produces dashboard rollups by initiative tag and department, with approved, held, warning, and blocker counts. | +| Custom tags or flags for internal initiatives | Validates controlled tag policies such as `GRANT-TRACKED`, `DOCTORAL-WORK`, and `PUBLIC-INITIATIVE`. | +| Contributor and productivity analytics integrity | Holds tags that lack owner approval, required evidence, valid scope, or expiry before they can influence rollups. | +| Compliance tracking | Checks funder linkage, open-access status, reproducibility score thresholds, and restricted-data dashboard exposure. | +| API and webhooks | Emits signed webhook-ready governance events for each approved or held initiative tag. | +| Enterprise-scale oversight | Produces deterministic reviewer packets and audit digests suitable for institutional admin review. | + +## Non-Overlap Note + +This submission is distinct from the existing dashboard/export/webhook/compliance/identity/retention/data-residency/SLA/lab-inventory/secret-rotation/quota/API-change/connector-certification/incident/funder-reporting/AI-model-governance/dashboard-attribution submissions. It focuses specifically on institution-defined initiative tag governance before dashboard and webhook publication. diff --git a/enterprise-initiative-tag-governance/test.js b/enterprise-initiative-tag-governance/test.js new file mode 100644 index 0000000..f0f2953 --- /dev/null +++ b/enterprise-initiative-tag-governance/test.js @@ -0,0 +1,181 @@ +"use strict" + +const assert = require("node:assert/strict") +const { assessEnterpriseInitiativeTags, normalizeId } = require("./index") + +const now = "2026-05-20T00:00:00.000Z" + +{ + const result = assessEnterpriseInitiativeTags({ + now, + organizationId: "university-alpha", + policies: [ + { + id: "GRANT-TRACKED", + label: "Grant tracked", + allowedScopes: ["biology", "chemistry"], + requiresOwnerApproval: true, + requiresExpiry: true, + maxDurationDays: 540, + requiredEvidence: ["grant-award-letter"], + publishToDashboard: true, + funderId: "nih-r01-42", + minimumReproducibilityScore: 80, + openAccessRequired: true, + }, + { + id: "DOCTORAL-WORK", + label: "Doctoral work", + allowedScopes: ["biology"], + requiresOwnerApproval: true, + requiresExpiry: true, + publishToDashboard: true, + }, + ], + evidence: [{ id: "grant-award-letter" }], + projects: [ + { + id: "project-1", + title: "Single-cell atlas release", + department: "biology", + visibility: "public", + dataClassification: "open", + funderIds: ["nih-r01-42"], + compliance: { openAccessStatus: "open", reproducibilityScore: 92 }, + }, + ], + assignments: [ + { + projectId: "project-1", + tagId: "GRANT-TRACKED", + scope: "biology", + assignedAt: "2026-01-10T00:00:00.000Z", + expiresAt: "2026-12-31T00:00:00.000Z", + ownerApproval: true, + evidenceIds: ["grant-award-letter"], + }, + { + projectId: "project-1", + tagId: "DOCTORAL-WORK", + scope: "biology", + assignedAt: "2026-03-01T00:00:00.000Z", + expiresAt: "2026-11-30T00:00:00.000Z", + ownerApproval: true, + }, + ], + }) + + assert.equal(result.status, "ready") + assert.equal(result.summary.approved, 2) + assert.equal(result.dashboardRollups.byTag.find((rollup) => rollup.tagId === "grant-tracked").projectCount, 1) + assert.match(result.webhookEvents[0].signature, /^[0-9a-f]{64}$/) + assert.match(result.auditDigest, /^[0-9a-f]{64}$/) +} + +{ + const result = assessEnterpriseInitiativeTags({ + now, + policies: [ + { + id: "PUBLIC-INITIATIVE", + label: "Public initiative", + allowedScopes: ["organization"], + requiresOwnerApproval: true, + requiresExpiry: true, + publishToDashboard: true, + restrictedDataAllowed: false, + }, + ], + projects: [ + { + id: "project-private", + title: "Embargoed sponsor project", + department: "oncology", + visibility: "private", + dataClassification: "restricted", + compliance: { openAccessStatus: "embargoed", reproducibilityScore: 70 }, + }, + ], + assignments: [ + { + projectId: "project-private", + tagId: "PUBLIC-INITIATIVE", + scope: "organization", + assignedAt: "2025-01-01T00:00:00.000Z", + expiresAt: "2026-01-01T00:00:00.000Z", + ownerApproval: false, + }, + ], + }) + + const report = result.assignmentReports[0] + assert.equal(result.status, "blocked") + assert.equal(report.status, "held") + assert.ok(report.blockers.includes("tag public-initiative is missing owner approval")) + assert.ok(report.blockers.includes("tag public-initiative expired on 2026-01-01")) + assert.ok(report.blockers.includes("tag public-initiative would expose a restricted project in admin dashboard rollups")) +} + +{ + const result = assessEnterpriseInitiativeTags({ + now, + policies: [ + { + id: "PUBLIC-RELEASE", + allowedScopes: ["physics"], + mutuallyExclusiveWith: ["INTERNAL-ONLY"], + publishToDashboard: true, + }, + { + id: "INTERNAL-ONLY", + allowedScopes: ["physics"], + mutuallyExclusiveWith: ["PUBLIC-RELEASE"], + publishToDashboard: false, + }, + ], + projects: [{ id: "project-2", department: "physics", visibility: "public", dataClassification: "open" }], + assignments: [ + { projectId: "project-2", tagId: "PUBLIC-RELEASE", scope: "physics" }, + { projectId: "project-2", tagId: "INTERNAL-ONLY", scope: "physics" }, + ], + }) + + assert.equal(result.status, "blocked") + assert.ok(result.assignmentReports[0].blockers.includes("tag public-release conflicts with internal-only")) + assert.ok(result.assignmentReports[1].blockers.includes("tag internal-only conflicts with public-release")) +} + +{ + const result = assessEnterpriseInitiativeTags({ + now, + policies: [ + { + id: "Funder Export", + allowedScopes: ["chemistry"], + funderId: "horizon-eu-9", + minimumReproducibilityScore: 90, + openAccessRequired: true, + publishToDashboard: true, + }, + ], + projects: [ + { + id: "project-3", + department: "chemistry", + visibility: "public", + dataClassification: "open", + funderIds: ["ukri-22"], + compliance: { openAccessStatus: "closed", reproducibilityScore: 82 }, + }, + ], + assignments: [{ projectId: "project-3", tagId: "Funder Export", scope: "chemistry" }], + }) + + assert.equal(normalizeId("Funder Export"), "funder-export") + assert.equal(result.status, "blocked") + assert.ok(result.assignmentReports[0].blockers.includes("tag funder-export requires funder horizon-eu-9")) + assert.ok(result.assignmentReports[0].warnings.includes("project reproducibility score 82 is below 90")) + assert.ok(result.assignmentReports[0].warnings.includes("project open access status is closed")) +} + +console.log("enterprise-initiative-tag-governance tests passed")