diff --git a/enterprise-compute-quota-governance/README.md b/enterprise-compute-quota-governance/README.md
new file mode 100644
index 0000000..466e9e5
--- /dev/null
+++ b/enterprise-compute-quota-governance/README.md
@@ -0,0 +1,50 @@
+# Enterprise Compute Quota Governance
+
+This module adds a focused Enterprise Tooling slice for institutional compute and
+storage governance. It helps admins see which labs, departments, and projects
+are approaching or exceeding GPU and storage allocations, then turns those
+signals into approval queue items, dashboard metrics, webhook events, and
+export-ready evidence.
+
+## Why this fits Issue #19
+
+The issue calls for admin dashboards, usage stats, custom flags, API/webhook
+integration, and export pipelines. This slice covers that surface without
+duplicating the existing open PRs for broad dashboards, export packaging,
+webhook replay, trust center, compliance packets, identity drift, retention,
+grant compliance, data residency, SLA monitoring, lab inventory, or secret
+rotation.
+
+## What is included
+
+- Portfolio dashboard metrics for GPU hours, storage, forecast cost, risk bands,
+ departments, cost centers, and top at-risk projects.
+- Deterministic quota evaluation for warning, critical, and blocked states.
+- Admin approval queue with requested decisions and action recommendations.
+- REST API catalog for dashboard, review queue, project detail, decision, and
+ export manifest routes, including service scopes and integration clients.
+- CSV quota risk register with project, lab, cost-center, quota, review queue,
+ and requested decision columns for finance and compliance exports.
+- Custom tag preservation for grant, doctoral, restricted-data, ELN sync,
+ open-science, and reproducibility initiatives.
+- Export manifest for institutional dashboards, finance chargeback ledgers,
+ compliance archives, workflow webhooks, and the REST API catalog.
+- HMAC-signed webhook payloads using synthetic sample data only.
+
+## Local verification
+
+```sh
+cd enterprise-compute-quota-governance
+npm run check
+npm test
+npm run demo
+```
+
+The implementation uses only Node.js built-ins and has no install step.
+
+## Demo output
+
+`npm run demo` prints the portfolio summary, review queue, REST API routes, CSV
+export metadata, and signed webhook event IDs. A static visual preview is
+available in `docs/demo.svg`; the PR also includes `docs/demo.mp4` as a short
+reviewer demo artifact.
diff --git a/enterprise-compute-quota-governance/demo.js b/enterprise-compute-quota-governance/demo.js
new file mode 100644
index 0000000..19e8fad
--- /dev/null
+++ b/enterprise-compute-quota-governance/demo.js
@@ -0,0 +1,38 @@
+"use strict";
+
+const sampleData = require("./sample-data.json");
+const { evaluateQuotaGovernance } = require("./index");
+
+const result = evaluateQuotaGovernance(sampleData);
+
+console.log("Enterprise Compute Quota Governance Demo");
+console.log(`Institution: ${result.institution}`);
+console.log(`Period: ${result.period}`);
+console.log("");
+console.log("Portfolio");
+console.log(`- Projects: ${result.dashboard.portfolio.projectCount}`);
+console.log(`- Forecast GPU hours: ${result.dashboard.portfolio.forecastGpuHours}`);
+console.log(`- Projected storage GB: ${result.dashboard.portfolio.projectedStorageGb}`);
+console.log(`- Forecast cost USD: ${result.dashboard.portfolio.forecastCostUsd}`);
+console.log(`- Risk counts: ${JSON.stringify(result.dashboard.portfolio.riskCounts)}`);
+console.log("");
+console.log("Top review queue");
+for (const item of result.approvalQueue) {
+ console.log(
+ `- ${item.severity.toUpperCase()} ${item.projectId}: ${item.requestedDecision} (${item.reasons.join("; ")})`
+ );
+}
+console.log("");
+console.log("API catalog");
+for (const endpoint of result.apiCatalog.endpoints) {
+ console.log(`- ${endpoint.method} ${endpoint.path} [${endpoint.scope}]`);
+}
+console.log("");
+console.log("CSV export");
+console.log(`- ${result.exportRegister.filename}: ${result.exportRegister.rows.length} rows`);
+console.log(`- Headers: ${result.exportRegister.headers.length}`);
+console.log("");
+console.log("Webhook events");
+for (const event of result.webhookEvents) {
+ console.log(`- ${event.id} ${event.signature.slice(0, 23)}...`);
+}
diff --git a/enterprise-compute-quota-governance/docs/demo.mp4 b/enterprise-compute-quota-governance/docs/demo.mp4
new file mode 100644
index 0000000..ad45722
Binary files /dev/null and b/enterprise-compute-quota-governance/docs/demo.mp4 differ
diff --git a/enterprise-compute-quota-governance/docs/demo.svg b/enterprise-compute-quota-governance/docs/demo.svg
new file mode 100644
index 0000000..2f72416
--- /dev/null
+++ b/enterprise-compute-quota-governance/docs/demo.svg
@@ -0,0 +1,38 @@
+
diff --git a/enterprise-compute-quota-governance/index.js b/enterprise-compute-quota-governance/index.js
new file mode 100644
index 0000000..403f768
--- /dev/null
+++ b/enterprise-compute-quota-governance/index.js
@@ -0,0 +1,612 @@
+"use strict";
+
+const crypto = require("node:crypto");
+
+const DEFAULT_POLICY = Object.freeze({
+ warningRatio: 0.8,
+ criticalRatio: 1,
+ blockRatio: 1.15,
+ reviewWindowDays: 7,
+ webhookSecret: "synthetic-quota-demo-secret"
+});
+
+const RISK_RANK = Object.freeze({
+ normal: 0,
+ warning: 1,
+ critical: 2,
+ blocked: 3
+});
+
+const EXPORT_REGISTER_HEADERS = Object.freeze([
+ "project_id",
+ "project_title",
+ "lab_id",
+ "lab_name",
+ "department",
+ "cost_center",
+ "principal_investigator",
+ "funder",
+ "risk",
+ "compute_allocated_gpu_hours",
+ "compute_forecast_gpu_hours",
+ "compute_overage_gpu_hours",
+ "storage_allocated_gb",
+ "storage_projected_gb",
+ "storage_overage_gb",
+ "forecast_cost_usd",
+ "tags",
+ "queue_id",
+ "requested_decision",
+ "due_in_days"
+]);
+
+function evaluateQuotaGovernance(input, policyOverrides = {}) {
+ if (!input || typeof input !== "object") {
+ throw new TypeError("evaluateQuotaGovernance requires an input object");
+ }
+
+ const policy = normalizePolicy(input.policy, policyOverrides);
+ const labs = Array.isArray(input.labs) ? input.labs : [];
+ const projectEvaluations = labs.flatMap((lab) =>
+ (Array.isArray(lab.projects) ? lab.projects : []).map((project) =>
+ evaluateProject(lab, project, policy)
+ )
+ );
+
+ const dashboard = buildDashboard(input, projectEvaluations);
+ const approvalQueue = buildApprovalQueue(projectEvaluations, policy);
+ const apiCatalog = buildApiCatalog(input, projectEvaluations, dashboard, approvalQueue);
+ const exportRegister = buildExportRegister(input, projectEvaluations, approvalQueue);
+ const exportManifest = buildExportManifest(
+ input,
+ projectEvaluations,
+ dashboard,
+ approvalQueue,
+ apiCatalog,
+ exportRegister
+ );
+ const complianceEvidence = buildComplianceEvidence(input, projectEvaluations, dashboard);
+
+ const result = {
+ institution: input.institution || "Unknown institution",
+ period: input.period || "unspecified",
+ generatedAt: input.generatedAt || new Date(0).toISOString(),
+ policy: redactPolicy(policy),
+ dashboard,
+ approvalQueue,
+ apiCatalog,
+ exportRegister,
+ exportManifest,
+ complianceEvidence
+ };
+
+ result.webhookEvents = buildWebhookEvents(result, policy.webhookSecret);
+ return result;
+}
+
+function normalizePolicy(...sources) {
+ const merged = Object.assign({}, DEFAULT_POLICY, ...sources.filter(Boolean));
+ for (const key of ["warningRatio", "criticalRatio", "blockRatio"]) {
+ if (!Number.isFinite(merged[key]) || merged[key] <= 0) {
+ throw new TypeError(`policy.${key} must be a positive number`);
+ }
+ }
+ if (!(merged.warningRatio < merged.criticalRatio && merged.criticalRatio < merged.blockRatio)) {
+ throw new TypeError("policy ratios must be ordered warning < critical < block");
+ }
+ if (!Number.isInteger(merged.reviewWindowDays) || merged.reviewWindowDays < 1) {
+ throw new TypeError("policy.reviewWindowDays must be a positive integer");
+ }
+ if (!merged.webhookSecret || typeof merged.webhookSecret !== "string") {
+ throw new TypeError("policy.webhookSecret must be a non-empty string");
+ }
+ return merged;
+}
+
+function evaluateProject(lab, project, policy) {
+ const compute = project.compute || {};
+ const storage = project.storage || {};
+ const computeForecast = toNumber(compute.usedGpuHours) + toNumber(compute.pendingGpuHours);
+ const storageForecast = toNumber(storage.projectedGb, storage.usedGb);
+ const computeRatio = quotaRatio(computeForecast, compute.allocatedGpuHours);
+ const storageRatio = quotaRatio(storageForecast, storage.allocatedGb);
+ const computeRisk = riskFromRatio(computeRatio, policy);
+ const storageRisk = riskFromRatio(storageRatio, policy);
+ const risk = maxRisk(computeRisk, storageRisk);
+ const computeOverage = Math.max(0, computeForecast - toNumber(compute.allocatedGpuHours));
+ const storageOverage = Math.max(0, storageForecast - toNumber(storage.allocatedGb));
+ const forecastCostUsd =
+ computeForecast * toNumber(compute.unitCostUsd) + storageForecast * toNumber(storage.unitCostUsd);
+
+ return {
+ projectId: project.id,
+ projectTitle: project.title,
+ labId: lab.id,
+ labName: lab.name,
+ department: lab.department,
+ costCenter: lab.costCenter,
+ principalInvestigator: project.principalInvestigator,
+ funder: project.funder,
+ tags: Array.isArray(project.tags) ? [...project.tags].sort() : [],
+ compute: {
+ allocatedGpuHours: toNumber(compute.allocatedGpuHours),
+ usedGpuHours: toNumber(compute.usedGpuHours),
+ pendingGpuHours: toNumber(compute.pendingGpuHours),
+ forecastGpuHours: computeForecast,
+ forecastRatio: roundRatio(computeRatio),
+ overageGpuHours: round(computeOverage)
+ },
+ storage: {
+ allocatedGb: toNumber(storage.allocatedGb),
+ usedGb: toNumber(storage.usedGb),
+ projectedGb: storageForecast,
+ forecastRatio: roundRatio(storageRatio),
+ overageGb: round(storageOverage)
+ },
+ forecastCostUsd: round(forecastCostUsd),
+ risk,
+ riskDrivers: riskDrivers(computeRisk, storageRisk, computeOverage, storageOverage),
+ recommendedActions: recommendedActions(computeRisk, storageRisk, project)
+ };
+}
+
+function buildDashboard(input, projectEvaluations) {
+ const riskCounts = { normal: 0, warning: 0, critical: 0, blocked: 0 };
+ const portfolio = {
+ projectCount: projectEvaluations.length,
+ allocatedGpuHours: 0,
+ forecastGpuHours: 0,
+ allocatedStorageGb: 0,
+ projectedStorageGb: 0,
+ forecastCostUsd: 0,
+ riskCounts
+ };
+ const departments = new Map();
+
+ for (const project of projectEvaluations) {
+ riskCounts[project.risk] += 1;
+ portfolio.allocatedGpuHours += project.compute.allocatedGpuHours;
+ portfolio.forecastGpuHours += project.compute.forecastGpuHours;
+ portfolio.allocatedStorageGb += project.storage.allocatedGb;
+ portfolio.projectedStorageGb += project.storage.projectedGb;
+ portfolio.forecastCostUsd += project.forecastCostUsd;
+
+ const department = departments.get(project.department) || {
+ department: project.department,
+ projectCount: 0,
+ atRiskProjects: 0,
+ forecastCostUsd: 0,
+ costCenters: []
+ };
+ department.projectCount += 1;
+ department.forecastCostUsd += project.forecastCostUsd;
+ if (project.risk !== "normal") {
+ department.atRiskProjects += 1;
+ }
+ if (project.costCenter && !department.costCenters.includes(project.costCenter)) {
+ department.costCenters.push(project.costCenter);
+ }
+ departments.set(project.department, department);
+ }
+
+ return {
+ institution: input.institution || "Unknown institution",
+ period: input.period || "unspecified",
+ portfolio: roundDashboard(portfolio),
+ departments: [...departments.values()]
+ .map((department) => ({
+ ...department,
+ forecastCostUsd: round(department.forecastCostUsd),
+ costCenters: department.costCenters.sort()
+ }))
+ .sort((a, b) => b.atRiskProjects - a.atRiskProjects || a.department.localeCompare(b.department)),
+ topAtRiskProjects: [...projectEvaluations]
+ .filter((project) => project.risk !== "normal")
+ .sort(compareRisk)
+ .slice(0, 5)
+ .map((project) => ({
+ projectId: project.projectId,
+ projectTitle: project.projectTitle,
+ labName: project.labName,
+ risk: project.risk,
+ computeForecastRatio: project.compute.forecastRatio,
+ storageForecastRatio: project.storage.forecastRatio,
+ recommendedActions: project.recommendedActions
+ }))
+ };
+}
+
+function buildApprovalQueue(projectEvaluations, policy) {
+ return [...projectEvaluations]
+ .filter((project) => project.risk !== "normal")
+ .sort(compareRisk)
+ .map((project, index) => ({
+ queueId: `quota-review-${String(index + 1).padStart(3, "0")}`,
+ projectId: project.projectId,
+ projectTitle: project.projectTitle,
+ labName: project.labName,
+ department: project.department,
+ owner: project.principalInvestigator,
+ severity: project.risk,
+ dueInDays: project.risk === "blocked" ? 1 : policy.reviewWindowDays,
+ reasons: project.riskDrivers,
+ requestedDecision: decisionForRisk(project.risk),
+ recommendedActions: project.recommendedActions
+ }));
+}
+
+function buildApiCatalog(input, projectEvaluations, dashboard, approvalQueue) {
+ const basePath = `/enterprise/quota-governance/${slug([
+ input.institution || "institution",
+ input.period || "period"
+ ])}`;
+
+ return {
+ version: "v1",
+ basePath,
+ authentication: "institutional-service-token",
+ scopes: ["enterprise:quota.read", "enterprise:quota.review", "enterprise:quota.export"],
+ endpoints: [
+ {
+ method: "GET",
+ path: `${basePath}/dashboard`,
+ scope: "enterprise:quota.read",
+ description: "Fetch portfolio and department quota metrics for admin dashboards.",
+ response: "dashboard",
+ recordCount: dashboard.portfolio.projectCount
+ },
+ {
+ method: "GET",
+ path: `${basePath}/reviews`,
+ scope: "enterprise:quota.review",
+ description: "List quota review decisions sorted by blocked, critical, and warning risk.",
+ response: "approvalQueue",
+ recordCount: approvalQueue.length
+ },
+ {
+ method: "GET",
+ path: `${basePath}/projects/{projectId}`,
+ scope: "enterprise:quota.read",
+ description: "Inspect a project compute/storage forecast, chargeback cost, and actions.",
+ response: "projectEvaluation",
+ recordCount: projectEvaluations.length
+ },
+ {
+ method: "POST",
+ path: `${basePath}/reviews/{queueId}/decision`,
+ scope: "enterprise:quota.review",
+ description: "Submit an admin approval, reduction, or escalation decision.",
+ response: "decisionReceipt",
+ recordCount: approvalQueue.length
+ },
+ {
+ method: "GET",
+ path: `${basePath}/export-manifest`,
+ scope: "enterprise:quota.export",
+ description: "Fetch routing metadata for dashboards, finance, compliance, and webhooks.",
+ response: "exportManifest",
+ recordCount: 6
+ }
+ ],
+ integrationClients: [
+ {
+ system: "institutional repositories",
+ examples: ["DSpace", "Invenio"],
+ uses: ["compliance evidence archive", "quota-aware export routing"]
+ },
+ {
+ system: "research operations dashboards",
+ examples: ["department admin console", "research office portal"],
+ uses: ["portfolio usage visibility", "approval queue triage"]
+ },
+ {
+ system: "finance and chargeback ledgers",
+ examples: ["ERP cost centers", "grant accounting"],
+ uses: ["forecast cost allocation", "department-level budget review"]
+ },
+ {
+ system: "workflow automation",
+ examples: ["webhook receivers", "ticketing systems"],
+ uses: ["review-required notifications", "decision receipt tracking"]
+ }
+ ]
+ };
+}
+
+function buildExportRegister(input, projectEvaluations, approvalQueue) {
+ const reviewByProject = new Map(approvalQueue.map((review) => [review.projectId, review]));
+ const rows = [...projectEvaluations].sort(compareRisk).map((project) => {
+ const review = reviewByProject.get(project.projectId);
+ return {
+ project_id: project.projectId,
+ project_title: project.projectTitle,
+ lab_id: project.labId,
+ lab_name: project.labName,
+ department: project.department,
+ cost_center: project.costCenter || "",
+ principal_investigator: project.principalInvestigator || "",
+ funder: project.funder || "",
+ risk: project.risk,
+ compute_allocated_gpu_hours: project.compute.allocatedGpuHours,
+ compute_forecast_gpu_hours: project.compute.forecastGpuHours,
+ compute_overage_gpu_hours: project.compute.overageGpuHours,
+ storage_allocated_gb: project.storage.allocatedGb,
+ storage_projected_gb: project.storage.projectedGb,
+ storage_overage_gb: project.storage.overageGb,
+ forecast_cost_usd: project.forecastCostUsd,
+ tags: project.tags.join(";"),
+ queue_id: review ? review.queueId : "",
+ requested_decision: review ? review.requestedDecision : "",
+ due_in_days: review ? review.dueInDays : ""
+ };
+ });
+
+ return {
+ filename: `${slug([
+ input.institution || "institution",
+ input.period || "period",
+ "quota-risk-register"
+ ])}.csv`,
+ headers: [...EXPORT_REGISTER_HEADERS],
+ rows,
+ csv: rowsToCsv(EXPORT_REGISTER_HEADERS, rows)
+ };
+}
+
+function rowsToCsv(headers, rows) {
+ return [headers.join(","), ...rows.map((row) => headers.map((header) => csvCell(row[header])).join(","))].join(
+ "\n"
+ );
+}
+
+function csvCell(value) {
+ if (value === null || value === undefined) {
+ return "";
+ }
+ const text = String(value);
+ if (/[",\n]/.test(text)) {
+ return `"${text.replace(/"/g, '""')}"`;
+ }
+ return text;
+}
+
+function buildExportManifest(input, projectEvaluations, dashboard, approvalQueue, apiCatalog, exportRegister) {
+ const atRiskProjects = projectEvaluations.filter((project) => project.risk !== "normal");
+ return {
+ manifestId: slug([input.institution || "institution", input.period || "period", "quota-governance"]),
+ generatedAt: input.generatedAt || new Date(0).toISOString(),
+ formats: ["json", "csv"],
+ targets: [
+ {
+ system: "institutional-admin-dashboard",
+ payload: "portfolio quota summary, department chargeback, top risk queue",
+ recordCount: dashboard.portfolio.projectCount
+ },
+ {
+ system: "finance-chargeback-ledger",
+ payload: "cost center usage forecast with compute and storage units",
+ recordCount: dashboard.departments.length
+ },
+ {
+ system: "csv-quota-risk-register",
+ payload: `${exportRegister.filename} with project, lab, quota, risk, queue, and decision columns`,
+ recordCount: exportRegister.rows.length
+ },
+ {
+ system: "compliance-evidence-archive",
+ payload: "review decisions, custom tags, funder-linked quota actions",
+ recordCount: atRiskProjects.length
+ },
+ {
+ system: "workflow-webhooks",
+ payload: "quota review required events for restricted or over-quota projects",
+ recordCount: approvalQueue.length
+ },
+ {
+ system: "enterprise-quota-rest-api",
+ payload: "endpoint catalog for dashboards, review queues, project detail, decisions, and export manifests",
+ recordCount: apiCatalog.endpoints.length
+ }
+ ]
+ };
+}
+
+function buildComplianceEvidence(input, projectEvaluations, dashboard) {
+ const flaggedTags = [...new Set(projectEvaluations.flatMap((project) => project.tags))].sort();
+ return {
+ requirementMap: [
+ "Organization-wide admin dashboard for projects, departments, cost centers, and risk queue",
+ "Usage stats for compute, storage, pending jobs, forecast overage, and chargeback",
+ "Custom tags retained for grant, doctoral, restricted-data, and open-science initiatives",
+ "REST API catalog for dashboard, review queue, project detail, decision, and export routes",
+ "Webhook-ready review events with deterministic HMAC signatures",
+ "Export manifest for institutional dashboards, finance ledgers, and compliance archives"
+ ],
+ customTags: flaggedTags,
+ fundersRepresented: [...new Set(projectEvaluations.map((project) => project.funder).filter(Boolean))].sort(),
+ reviewSummary: {
+ generatedAt: input.generatedAt || new Date(0).toISOString(),
+ riskCounts: dashboard.portfolio.riskCounts,
+ atRiskProjectCount: projectEvaluations.filter((project) => project.risk !== "normal").length
+ }
+ };
+}
+
+function buildWebhookEvents(result, secret) {
+ return result.approvalQueue.map((item) => {
+ const payload = {
+ eventType: "enterprise.quota_review_required",
+ institution: result.institution,
+ period: result.period,
+ queueId: item.queueId,
+ projectId: item.projectId,
+ severity: item.severity,
+ requestedDecision: item.requestedDecision,
+ dueInDays: item.dueInDays
+ };
+ return {
+ id: `${payload.eventType}.${item.queueId}`,
+ topic: payload.eventType,
+ destination: "institutional-admin-api",
+ payload,
+ signature: `sha256=${signPayload(payload, secret)}`
+ };
+ });
+}
+
+function signPayload(payload, secret) {
+ return crypto.createHmac("sha256", secret).update(stableStringify(payload)).digest("hex");
+}
+
+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 quotaRatio(forecast, allocated) {
+ const allocatedNumber = toNumber(allocated);
+ if (allocatedNumber <= 0) {
+ return forecast > 0 ? Number.POSITIVE_INFINITY : 0;
+ }
+ return forecast / allocatedNumber;
+}
+
+function riskFromRatio(value, policy) {
+ if (value >= policy.blockRatio) {
+ return "blocked";
+ }
+ if (value >= policy.criticalRatio) {
+ return "critical";
+ }
+ if (value >= policy.warningRatio) {
+ return "warning";
+ }
+ return "normal";
+}
+
+function maxRisk(...risks) {
+ return risks.sort((a, b) => RISK_RANK[b] - RISK_RANK[a])[0];
+}
+
+function riskDrivers(computeRisk, storageRisk, computeOverage, storageOverage) {
+ const drivers = [];
+ if (computeRisk !== "normal") {
+ drivers.push(
+ computeOverage > 0
+ ? `compute forecast exceeds allocation by ${round(computeOverage)} GPU hours`
+ : `compute forecast is in ${computeRisk} band`
+ );
+ }
+ if (storageRisk !== "normal") {
+ drivers.push(
+ storageOverage > 0
+ ? `storage forecast exceeds allocation by ${round(storageOverage)} GB`
+ : `storage forecast is in ${storageRisk} band`
+ );
+ }
+ return drivers;
+}
+
+function recommendedActions(computeRisk, storageRisk, project) {
+ const actions = new Set();
+ const projectTags = Array.isArray(project.tags) ? project.tags : [];
+ const overallRisk = maxRisk(computeRisk, storageRisk);
+
+ if (overallRisk === "blocked") {
+ actions.add("pause-new-workloads-until-admin-review");
+ actions.add("route-to-department-head");
+ } else if (overallRisk === "critical") {
+ actions.add("require-quota-extension-approval");
+ } else if (overallRisk === "warning") {
+ actions.add("notify-lab-owner-before-next-billing-cycle");
+ }
+
+ if (storageRisk !== "normal") {
+ actions.add("review-cold-storage-and-export-archive-options");
+ }
+ if (computeRisk !== "normal") {
+ actions.add("cap-pending-gpu-jobs-until-approval");
+ }
+ if (projectTags.includes("RESTRICTED-DATA")) {
+ actions.add("attach-restricted-data-compliance-evidence");
+ }
+ if (projectTags.includes("GRANT-TRACKED")) {
+ actions.add("preserve-funder-mandate-audit-trail");
+ }
+
+ return [...actions].sort();
+}
+
+function decisionForRisk(risk) {
+ if (risk === "blocked") {
+ return "block-and-escalate";
+ }
+ if (risk === "critical") {
+ return "approve-extension-or-reduce-forecast";
+ }
+ return "review-before-next-allocation-cycle";
+}
+
+function compareRisk(a, b) {
+ return (
+ RISK_RANK[b.risk] - RISK_RANK[a.risk] ||
+ b.forecastCostUsd - a.forecastCostUsd ||
+ a.projectId.localeCompare(b.projectId)
+ );
+}
+
+function roundDashboard(portfolio) {
+ return {
+ ...portfolio,
+ allocatedGpuHours: round(portfolio.allocatedGpuHours),
+ forecastGpuHours: round(portfolio.forecastGpuHours),
+ allocatedStorageGb: round(portfolio.allocatedStorageGb),
+ projectedStorageGb: round(portfolio.projectedStorageGb),
+ forecastCostUsd: round(portfolio.forecastCostUsd)
+ };
+}
+
+function redactPolicy(policy) {
+ const copy = { ...policy };
+ copy.webhookSecret = "";
+ return copy;
+}
+
+function toNumber(value, fallback = 0) {
+ const candidate = value ?? fallback;
+ return Number.isFinite(Number(candidate)) ? Number(candidate) : 0;
+}
+
+function round(value) {
+ return Math.round(value * 100) / 100;
+}
+
+function roundRatio(value) {
+ if (!Number.isFinite(value)) {
+ return value;
+ }
+ return Math.round(value * 1000) / 1000;
+}
+
+function slug(parts) {
+ return parts
+ .join("-")
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/(^-|-$)/g, "");
+}
+
+module.exports = {
+ DEFAULT_POLICY,
+ evaluateQuotaGovernance,
+ signPayload,
+ stableStringify
+};
diff --git a/enterprise-compute-quota-governance/package.json b/enterprise-compute-quota-governance/package.json
new file mode 100644
index 0000000..7f7e5fc
--- /dev/null
+++ b/enterprise-compute-quota-governance/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "enterprise-compute-quota-governance",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Institutional compute and storage quota governance for SCIBASE Enterprise Tooling.",
+ "main": "index.js",
+ "scripts": {
+ "check": "node --check index.js && node --check demo.js && node --check test.js",
+ "demo": "node demo.js",
+ "test": "node test.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "license": "MIT"
+}
diff --git a/enterprise-compute-quota-governance/requirements-map.md b/enterprise-compute-quota-governance/requirements-map.md
new file mode 100644
index 0000000..207eba5
--- /dev/null
+++ b/enterprise-compute-quota-governance/requirements-map.md
@@ -0,0 +1,19 @@
+# Issue #19 Requirement Map
+
+| Issue #19 capability | Compute quota governance coverage |
+| --- | --- |
+| Organization-wide admin dashboards | `dashboard.portfolio`, `dashboard.departments`, and `dashboard.topAtRiskProjects` summarize institutional usage, cost centers, department risk, and review queues. |
+| Usage stats | The evaluator reports allocated GPU hours, forecast GPU hours, allocated storage, projected storage, pending jobs, overages, forecast cost, and risk bands. |
+| Productivity and compliance metrics | Custom project tags, funders, restricted-data flags, grant-tracked work, and risk counts are preserved in `complianceEvidence`. |
+| Custom tags or flags | Sample projects include `GRANT-TRACKED`, `DOCTORAL-WORK`, `RESTRICTED-DATA`, `ELN-SYNC`, `OPEN-SCIENCE`, and `REPRODUCIBILITY`; the evidence packet exports the distinct tag set. |
+| API and webhooks | `apiCatalog` defines service-token REST routes, scopes, endpoint metadata, and integration clients; `webhookEvents` are deterministic `enterprise.quota_review_required` payloads with HMAC signatures generated by `signPayload`. |
+| Export pipelines | `exportManifest.targets` covers admin dashboards, finance chargeback ledgers, the generated CSV quota risk register, compliance evidence archives, workflow webhook delivery, and the REST API catalog. |
+| Institutional use case | The sample data models labs, departments, PIs, funders, cost centers, compute allocations, storage allocations, and governance actions for a research university. |
+
+## Differentiation from reviewed open PRs
+
+This is intentionally scoped to compute and storage quota governance. It does
+not reimplement broad enterprise dashboards, export pipelines, compliance
+packet generation, webhook replay, identity provisioning drift, retention/legal
+hold controls, grant portfolio compliance, data residency, SLA uptime, lab
+inventory sync, or API secret rotation.
diff --git a/enterprise-compute-quota-governance/sample-data.json b/enterprise-compute-quota-governance/sample-data.json
new file mode 100644
index 0000000..3329c9b
--- /dev/null
+++ b/enterprise-compute-quota-governance/sample-data.json
@@ -0,0 +1,108 @@
+{
+ "institution": "Northbridge Research University",
+ "period": "2026-Q2",
+ "generatedAt": "2026-05-17T00:00:00.000Z",
+ "policy": {
+ "warningRatio": 0.8,
+ "criticalRatio": 1,
+ "blockRatio": 1.15,
+ "reviewWindowDays": 7,
+ "webhookSecret": "synthetic-quota-demo-secret"
+ },
+ "labs": [
+ {
+ "id": "lab-neuro",
+ "name": "Neural Systems Lab",
+ "department": "Neuroscience",
+ "costCenter": "NRU-NEURO-410",
+ "owners": ["Dr. Mina Patel", "research-ops@nru.example"],
+ "projects": [
+ {
+ "id": "neuro-open-atlas",
+ "title": "Open Neural Atlas",
+ "principalInvestigator": "Dr. Mina Patel",
+ "funder": "NIH",
+ "tags": ["GRANT-TRACKED", "OPEN-SCIENCE"],
+ "compute": {
+ "allocatedGpuHours": 1200,
+ "usedGpuHours": 910,
+ "pendingGpuHours": 240,
+ "unitCostUsd": 3.4
+ },
+ "storage": {
+ "allocatedGb": 8000,
+ "usedGb": 6200,
+ "projectedGb": 6900,
+ "unitCostUsd": 0.03
+ }
+ },
+ {
+ "id": "microscopy-foundation-model",
+ "title": "Microscopy Foundation Model",
+ "principalInvestigator": "Dr. Leon Watts",
+ "funder": "NSF",
+ "tags": ["DOCTORAL-WORK", "RESTRICTED-DATA"],
+ "compute": {
+ "allocatedGpuHours": 900,
+ "usedGpuHours": 980,
+ "pendingGpuHours": 180,
+ "unitCostUsd": 4.1
+ },
+ "storage": {
+ "allocatedGb": 5000,
+ "usedGb": 5100,
+ "projectedGb": 5600,
+ "unitCostUsd": 0.04
+ }
+ }
+ ]
+ },
+ {
+ "id": "lab-climate",
+ "name": "Climate Informatics Group",
+ "department": "Earth Systems",
+ "costCenter": "NRU-EARTH-225",
+ "owners": ["Dr. Rowan Ellis", "earth-admin@nru.example"],
+ "projects": [
+ {
+ "id": "river-sensor-elns",
+ "title": "River Sensor ELN Sync",
+ "principalInvestigator": "Dr. Rowan Ellis",
+ "funder": "Horizon EU",
+ "tags": ["ELN-SYNC", "GRANT-TRACKED"],
+ "compute": {
+ "allocatedGpuHours": 300,
+ "usedGpuHours": 121,
+ "pendingGpuHours": 40,
+ "unitCostUsd": 2.9
+ },
+ "storage": {
+ "allocatedGb": 1000,
+ "usedGb": 870,
+ "projectedGb": 930,
+ "unitCostUsd": 0.02
+ }
+ },
+ {
+ "id": "climate-preprint-replication",
+ "title": "Climate Preprint Replication",
+ "principalInvestigator": "Dr. Sabine Klein",
+ "funder": "UKRI",
+ "tags": ["OPEN-SCIENCE", "REPRODUCIBILITY"],
+ "compute": {
+ "allocatedGpuHours": 800,
+ "usedGpuHours": 770,
+ "pendingGpuHours": 80,
+ "unitCostUsd": 3.1
+ },
+ "storage": {
+ "allocatedGb": 2200,
+ "usedGb": 1450,
+ "projectedGb": 1580,
+ "unitCostUsd": 0.025
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/enterprise-compute-quota-governance/test.js b/enterprise-compute-quota-governance/test.js
new file mode 100644
index 0000000..78a34aa
--- /dev/null
+++ b/enterprise-compute-quota-governance/test.js
@@ -0,0 +1,119 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const sampleData = require("./sample-data.json");
+const { evaluateQuotaGovernance, signPayload, stableStringify } = require("./index");
+
+const result = evaluateQuotaGovernance(sampleData);
+
+assert.equal(result.institution, "Northbridge Research University");
+assert.equal(result.period, "2026-Q2");
+assert.deepEqual(result.policy.webhookSecret, "");
+
+assert.equal(result.dashboard.portfolio.projectCount, 4);
+assert.deepEqual(result.dashboard.portfolio.riskCounts, {
+ normal: 0,
+ warning: 2,
+ critical: 1,
+ blocked: 1
+});
+
+assert.equal(result.approvalQueue.length, 4);
+assert.equal(result.approvalQueue[0].projectId, "microscopy-foundation-model");
+assert.equal(result.approvalQueue[0].severity, "blocked");
+assert.equal(result.approvalQueue[0].requestedDecision, "block-and-escalate");
+assert.ok(
+ result.approvalQueue[0].recommendedActions.includes("attach-restricted-data-compliance-evidence")
+);
+
+assert.equal(
+ result.apiCatalog.basePath,
+ "/enterprise/quota-governance/northbridge-research-university-2026-q2"
+);
+assert.deepEqual(result.apiCatalog.scopes, [
+ "enterprise:quota.read",
+ "enterprise:quota.review",
+ "enterprise:quota.export"
+]);
+assert.deepEqual(
+ result.apiCatalog.endpoints.map((endpoint) => `${endpoint.method} ${endpoint.path}`),
+ [
+ "GET /enterprise/quota-governance/northbridge-research-university-2026-q2/dashboard",
+ "GET /enterprise/quota-governance/northbridge-research-university-2026-q2/reviews",
+ "GET /enterprise/quota-governance/northbridge-research-university-2026-q2/projects/{projectId}",
+ "POST /enterprise/quota-governance/northbridge-research-university-2026-q2/reviews/{queueId}/decision",
+ "GET /enterprise/quota-governance/northbridge-research-university-2026-q2/export-manifest"
+ ]
+);
+assert.ok(
+ result.apiCatalog.integrationClients.some((client) =>
+ client.examples.includes("DSpace")
+ )
+);
+assert.equal(
+ result.apiCatalog.endpoints.find((endpoint) => endpoint.response === "exportManifest").recordCount,
+ result.exportManifest.targets.length
+);
+
+const climateReview = result.approvalQueue.find(
+ (item) => item.projectId === "climate-preprint-replication"
+);
+assert.equal(climateReview.severity, "critical");
+assert.equal(climateReview.requestedDecision, "approve-extension-or-reduce-forecast");
+
+assert.deepEqual(
+ result.exportManifest.targets.map((target) => target.system),
+ [
+ "institutional-admin-dashboard",
+ "finance-chargeback-ledger",
+ "csv-quota-risk-register",
+ "compliance-evidence-archive",
+ "workflow-webhooks",
+ "enterprise-quota-rest-api"
+ ]
+);
+assert.equal(result.exportManifest.formats.includes("csv"), true);
+assert.equal(result.exportRegister.filename, "northbridge-research-university-2026-q2-quota-risk-register.csv");
+assert.equal(result.exportRegister.rows.length, 4);
+assert.equal(result.exportRegister.rows[0].project_id, "microscopy-foundation-model");
+assert.equal(result.exportRegister.rows[0].queue_id, "quota-review-001");
+assert.equal(result.exportRegister.rows[0].requested_decision, "block-and-escalate");
+assert.equal(result.exportRegister.rows[0].tags, "DOCTORAL-WORK;RESTRICTED-DATA");
+assert.equal(
+ result.exportRegister.csv.split("\n")[0],
+ "project_id,project_title,lab_id,lab_name,department,cost_center,principal_investigator,funder,risk,compute_allocated_gpu_hours,compute_forecast_gpu_hours,compute_overage_gpu_hours,storage_allocated_gb,storage_projected_gb,storage_overage_gb,forecast_cost_usd,tags,queue_id,requested_decision,due_in_days"
+);
+assert.ok(result.exportRegister.csv.includes("microscopy-foundation-model"));
+
+assert.ok(result.complianceEvidence.customTags.includes("GRANT-TRACKED"));
+assert.ok(result.complianceEvidence.customTags.includes("RESTRICTED-DATA"));
+assert.ok(
+ result.complianceEvidence.requirementMap.some((line) =>
+ line.includes("REST API catalog")
+ )
+);
+assert.ok(
+ result.complianceEvidence.requirementMap.some((line) =>
+ line.includes("Webhook-ready review events")
+ )
+);
+
+const firstEvent = result.webhookEvents[0];
+assert.equal(firstEvent.topic, "enterprise.quota_review_required");
+assert.equal(firstEvent.destination, "institutional-admin-api");
+assert.equal(
+ firstEvent.signature,
+ `sha256=${signPayload(firstEvent.payload, sampleData.policy.webhookSecret)}`
+);
+
+assert.equal(
+ stableStringify({ b: 2, a: [3, { c: "x" }] }),
+ '{"a":[3,{"c":"x"}],"b":2}'
+);
+
+assert.throws(
+ () => evaluateQuotaGovernance({ policy: { warningRatio: 1, criticalRatio: 0.8, blockRatio: 1.1 } }),
+ /warning < critical < block/
+);
+
+console.log("enterprise-compute-quota-governance tests passed");