From caf5ecb5d31578cf25fff017c2da9f6b864c153b Mon Sep 17 00:00:00 2001 From: Ethan Miller Date: Fri, 22 May 2026 02:41:04 -0400 Subject: [PATCH] Add enterprise usage cost allocation guard --- .../README.md | 34 ++ .../acceptance-notes.md | 21 ++ .../demo.js | 31 ++ .../index.js | 344 ++++++++++++++++++ .../package.json | 13 + .../reports/demo.mp4 | Bin 0 -> 12119 bytes .../reports/reviewer-packet.md | 88 +++++ .../reports/summary.json | 175 +++++++++ .../reports/summary.svg | 29 ++ .../requirements-map.md | 19 + .../sample-data.js | 132 +++++++ .../test.js | 46 +++ 12 files changed, 932 insertions(+) create mode 100644 enterprise-usage-cost-allocation-guard/README.md create mode 100644 enterprise-usage-cost-allocation-guard/acceptance-notes.md create mode 100644 enterprise-usage-cost-allocation-guard/demo.js create mode 100644 enterprise-usage-cost-allocation-guard/index.js create mode 100644 enterprise-usage-cost-allocation-guard/package.json create mode 100644 enterprise-usage-cost-allocation-guard/reports/demo.mp4 create mode 100644 enterprise-usage-cost-allocation-guard/reports/reviewer-packet.md create mode 100644 enterprise-usage-cost-allocation-guard/reports/summary.json create mode 100644 enterprise-usage-cost-allocation-guard/reports/summary.svg create mode 100644 enterprise-usage-cost-allocation-guard/requirements-map.md create mode 100644 enterprise-usage-cost-allocation-guard/sample-data.js create mode 100644 enterprise-usage-cost-allocation-guard/test.js diff --git a/enterprise-usage-cost-allocation-guard/README.md b/enterprise-usage-cost-allocation-guard/README.md new file mode 100644 index 00000000..9beecc05 --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/README.md @@ -0,0 +1,34 @@ +# Enterprise Usage Cost Allocation Guard + +This module provides a focused Enterprise Tooling slice for SCIBASE issue #19. +It reconciles synthetic storage, compute, submission, and AI-review usage against +lab cost centers, grants, owner sponsorship, funding windows, and private-project +visibility rules before those costs are published to institutional dashboards or +grant chargeback exports. + +## What It Covers + +- Enterprise admin dashboard usage allocation readiness. +- Grant and cost-center chargeback validation. +- Hold decisions for missing evidence, unknown ownership, and out-of-window spend. +- Reallocation decisions for disallowed cost centers or usage types. +- Private-project review decisions that suppress sensitive project titles. +- Deterministic audit digests and reviewer packets. + +## Run + +```bash +npm run check +npm test +npm run demo +``` + +Generated artifacts are written to `reports/`: + +- `summary.json` +- `reviewer-packet.md` +- `summary.svg` +- `demo.mp4` + +The data is synthetic and does not contact live billing, grant, identity, or +provider systems. diff --git a/enterprise-usage-cost-allocation-guard/acceptance-notes.md b/enterprise-usage-cost-allocation-guard/acceptance-notes.md new file mode 100644 index 00000000..3dd7dc60 --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/acceptance-notes.md @@ -0,0 +1,21 @@ +# Acceptance Notes + +The guard accepts synthetic enterprise usage records and classifies each record +as one of: + +- `approved`: ready for dashboard and grant chargeback export. +- `hold`: missing required evidence, ownership, amount, or funding-window validity. +- `reallocate`: valid usage that must move to a permitted cost center. +- `private_review`: valid private/restricted project usage that requires + aggregate-only admin handling before publication. + +Validation commands: + +```bash +npm run check +npm test +npm run demo +ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height,pix_fmt -of default=noprint_wrappers=1 reports/demo.mp4 +git diff --check +git diff --cached --check +``` diff --git a/enterprise-usage-cost-allocation-guard/demo.js b/enterprise-usage-cost-allocation-guard/demo.js new file mode 100644 index 00000000..0890d18c --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/demo.js @@ -0,0 +1,31 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { + analyzeEnterpriseUsage, + renderMarkdownReport, + renderSvgSummary, +} = require("./index"); +const { sampleEnterpriseUsagePacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const result = analyzeEnterpriseUsage(sampleEnterpriseUsagePacket, { + asOf: "2026-05-22T12:00:00.000Z", +}); + +fs.writeFileSync( + path.join(reportsDir, "summary.json"), + `${JSON.stringify(result, null, 2)}\n` +); +fs.writeFileSync( + path.join(reportsDir, "reviewer-packet.md"), + renderMarkdownReport(result) +); +fs.writeFileSync( + path.join(reportsDir, "summary.svg"), + renderSvgSummary(result) +); + +console.log("enterprise usage cost allocation guard demo artifacts written"); +console.log(`audit digest: ${result.auditDigest}`); diff --git a/enterprise-usage-cost-allocation-guard/index.js b/enterprise-usage-cost-allocation-guard/index.js new file mode 100644 index 00000000..86101424 --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/index.js @@ -0,0 +1,344 @@ +const crypto = require("node:crypto"); + +const APPROVED = "approved"; +const HOLD = "hold"; +const REALLOCATE = "reallocate"; +const PRIVATE_REVIEW = "private_review"; + +function parseDate(value, label) { + if (!value) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + throw new Error(`invalid date for ${label}`); + } + return date; +} + +function requireFields(value, fields, label) { + for (const field of fields) { + if (value[field] === undefined || value[field] === null || value[field] === "") { + throw new Error(`missing required ${label} field: ${field}`); + } + } +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + + return JSON.stringify(value); +} + +function createAuditDigest(result) { + const payload = { + asOf: result.asOf, + totals: result.totals, + decisions: result.decisions.map((decision) => ({ + id: decision.id, + status: decision.status, + labId: decision.labId, + grantId: decision.grantId, + amountUsd: decision.amountUsd, + reasons: decision.reasons, + })), + }; + + return crypto.createHash("sha256").update(stableStringify(payload)).digest("hex"); +} + +function buildLookup(items, key) { + const lookup = new Map(); + for (const item of items || []) { + requireFields(item, [key], key); + lookup.set(item[key], item); + } + return lookup; +} + +function withinWindow(date, start, end) { + if (start && date < start) return false; + if (end && date > end) return false; + return true; +} + +function evaluateUsage(record, context) { + requireFields( + record, + [ + "id", + "labId", + "projectId", + "projectVisibility", + "usageType", + "amountUsd", + "costCenterId", + "grantId", + "ownerId", + "occurredAt", + ], + "usage" + ); + + const lab = context.labs.get(record.labId); + const grant = context.grants.get(record.grantId); + const occurredAt = parseDate(record.occurredAt, `occurredAt for ${record.id}`); + const reasons = []; + const actions = []; + const base = { + id: record.id, + labId: record.labId, + projectId: record.projectId, + projectVisibility: record.projectVisibility, + usageType: record.usageType, + amountUsd: Number(record.amountUsd), + costCenterId: record.costCenterId, + grantId: record.grantId, + ownerId: record.ownerId, + approvedAmountUsd: 0, + heldAmountUsd: 0, + reallocatedAmountUsd: 0, + reasons, + actions, + }; + + if (!lab) reasons.push("lab is not registered for enterprise allocation"); + if (!grant) reasons.push("grant is not registered for chargeback"); + if (!Array.isArray(record.evidenceIds) || record.evidenceIds.length === 0) { + reasons.push("usage record has no immutable job/export evidence"); + } + + if (!Number.isFinite(base.amountUsd) || base.amountUsd <= 0) { + reasons.push("amount must be a positive number"); + } + + if (reasons.length > 0) { + return { + ...base, + status: HOLD, + heldAmountUsd: base.amountUsd, + actions: [ + "Hold chargeback from dashboard totals", + "Request missing lab, grant, amount, or evidence mapping", + ], + }; + } + + const grantStart = parseDate(grant.startsAt, `grant ${grant.grantId} startsAt`); + const grantEnd = parseDate(grant.endsAt, `grant ${grant.grantId} endsAt`); + const labAllowsCostCenter = lab.activeCostCenters.includes(record.costCenterId); + const grantAllowsCostCenter = grant.allowedCostCenters.includes(record.costCenterId); + const grantAllowsUsage = grant.allowedUsageTypes.includes(record.usageType); + const ownerAllowed = lab.ownerIds.includes(record.ownerId); + + if (!withinWindow(occurredAt, grantStart, grantEnd)) { + return { + ...base, + status: HOLD, + heldAmountUsd: base.amountUsd, + reasons: ["usage occurred outside the grant funding window"], + actions: [ + "Exclude from automated grant chargeback", + "Route to enterprise finance review", + ], + }; + } + + if (!ownerAllowed) { + return { + ...base, + status: HOLD, + heldAmountUsd: base.amountUsd, + reasons: ["usage owner is not authorized for the lab cost center"], + actions: [ + "Hold chargeback", + "Ask lab admin to confirm owner sponsorship", + ], + }; + } + + if (!labAllowsCostCenter || !grantAllowsCostCenter || !grantAllowsUsage) { + return { + ...base, + status: REALLOCATE, + reallocatedAmountUsd: base.amountUsd, + reasons: [ + !labAllowsCostCenter + ? "cost center is not active for this lab" + : "grant does not permit this cost center or usage type", + ], + actions: [ + "Remove from grant spend until reallocated", + "Suggest a permitted lab cost center before dashboard publication", + ], + }; + } + + if (record.projectVisibility === "private" || record.privateDataClass) { + return { + ...base, + status: PRIVATE_REVIEW, + heldAmountUsd: base.amountUsd, + reasons: [ + "private or restricted project usage needs admin visibility review before chargeback", + ], + actions: [ + "Show aggregate spend only", + "Suppress project title from department dashboard", + "Require admin approval before grant closeout export", + ], + }; + } + + return { + ...base, + status: APPROVED, + approvedAmountUsd: base.amountUsd, + reasons: [ + "usage evidence, grant window, cost center, usage type, and owner all match", + ], + actions: [ + "Publish in admin dashboard allocation", + "Include in grant chargeback export packet", + ], + }; +} + +function analyzeEnterpriseUsage(packet, options = {}) { + requireFields(packet, ["labs", "grants", "usageRecords"], "enterprise packet"); + + const context = { + labs: buildLookup(packet.labs, "labId"), + grants: buildLookup(packet.grants, "grantId"), + }; + const asOf = parseDate(options.asOf || packet.asOf || new Date().toISOString(), "asOf"); + const decisions = packet.usageRecords.map((record) => evaluateUsage(record, context)); + const totals = decisions.reduce( + (acc, decision) => { + acc.totalRecords += 1; + acc.totalAmountUsd += decision.amountUsd; + acc.approvedAmountUsd += decision.approvedAmountUsd; + acc.heldAmountUsd += decision.heldAmountUsd; + acc.reallocatedAmountUsd += decision.reallocatedAmountUsd; + acc.byStatus[decision.status] = (acc.byStatus[decision.status] || 0) + 1; + return acc; + }, + { + totalRecords: 0, + totalAmountUsd: 0, + approvedAmountUsd: 0, + heldAmountUsd: 0, + reallocatedAmountUsd: 0, + byStatus: {}, + } + ); + + const result = { + asOf: asOf.toISOString(), + totals, + decisions, + }; + + return { + ...result, + auditDigest: createAuditDigest(result), + }; +} + +function renderMarkdownReport(result) { + const lines = [ + "# Enterprise Usage Cost Allocation Guard", + "", + `As of: ${result.asOf}`, + `Audit digest: \`${result.auditDigest}\``, + "", + "## Totals", + "", + `- Usage records: ${result.totals.totalRecords}`, + `- Total usage amount: $${result.totals.totalAmountUsd.toFixed(2)}`, + `- Approved amount: $${result.totals.approvedAmountUsd.toFixed(2)}`, + `- Held amount: $${result.totals.heldAmountUsd.toFixed(2)}`, + `- Reallocated amount: $${result.totals.reallocatedAmountUsd.toFixed(2)}`, + ]; + + for (const [status, count] of Object.entries(result.totals.byStatus).sort()) { + lines.push(`- ${status}: ${count}`); + } + + lines.push("", "## Allocation Decisions", ""); + for (const decision of result.decisions) { + lines.push( + `### ${decision.id}`, + "", + `- Project: ${decision.projectId}`, + `- Lab: ${decision.labId}`, + `- Grant: ${decision.grantId}`, + `- Amount: $${decision.amountUsd.toFixed(2)}`, + `- Status: ${decision.status}`, + `- Reasons: ${decision.reasons.join("; ")}`, + `- Actions: ${decision.actions.join("; ")}`, + "" + ); + } + + return `${lines.join("\n").trimEnd()}\n`; +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderSvgSummary(result) { + const statuses = Object.entries(result.totals.byStatus).sort(); + const rows = statuses + .map(([status, count], index) => { + const y = 160 + index * 46; + const width = Math.max(44, count * 92); + return `${escapeXml(status)} + + ${count}`; + }) + .join("\n "); + + return ` + + + + Enterprise Usage Cost Allocation Guard + Approved $${result.totals.approvedAmountUsd.toFixed(2)} / Held $${result.totals.heldAmountUsd.toFixed(2)} / Reallocate $${result.totals.reallocatedAmountUsd.toFixed(2)} + ${rows} + audit ${escapeXml(result.auditDigest.slice(0, 48))} + +`; +} + +module.exports = { + APPROVED, + HOLD, + REALLOCATE, + PRIVATE_REVIEW, + analyzeEnterpriseUsage, + createAuditDigest, + renderMarkdownReport, + renderSvgSummary, +}; diff --git a/enterprise-usage-cost-allocation-guard/package.json b/enterprise-usage-cost-allocation-guard/package.json new file mode 100644 index 00000000..812e53d4 --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "enterprise-usage-cost-allocation-guard", + "version": "1.0.0", + "private": true, + "description": "Synthetic enterprise usage cost-allocation and grant chargeback guard for SCIBASE issue #19.", + "main": "index.js", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js", + "test": "node test.js", + "demo": "node demo.js" + }, + "license": "MIT" +} diff --git a/enterprise-usage-cost-allocation-guard/reports/demo.mp4 b/enterprise-usage-cost-allocation-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..88fbd066679fe86c41fc1fa6e655ce0a33ece17f GIT binary patch literal 12119 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 literal 0 HcmV?d00001 diff --git a/enterprise-usage-cost-allocation-guard/reports/reviewer-packet.md b/enterprise-usage-cost-allocation-guard/reports/reviewer-packet.md new file mode 100644 index 00000000..8a8afa7f --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/reports/reviewer-packet.md @@ -0,0 +1,88 @@ +# Enterprise Usage Cost Allocation Guard + +As of: 2026-05-22T12:00:00.000Z +Audit digest: `d5139452c6df2c02f396578ad66c0441c4a3238307cf314683161bba42b25da7` + +## Totals + +- Usage records: 7 +- Total usage amount: $1965.00 +- Approved amount: $715.00 +- Held amount: $940.00 +- Reallocated amount: $310.00 +- approved: 2 +- hold: 2 +- private_review: 2 +- reallocate: 1 + +## Allocation Decisions + +### usage-001 + +- Project: proj-vision-atlas +- Lab: lab-neuro +- Grant: grant-nih-r01-vision +- Amount: $620.00 +- Status: approved +- Reasons: usage evidence, grant window, cost center, usage type, and owner all match +- Actions: Publish in admin dashboard allocation; Include in grant chargeback export packet + +### usage-002 + +- Project: proj-retina-review +- Lab: lab-neuro +- Grant: grant-nih-r01-vision +- Amount: $260.00 +- Status: private_review +- Reasons: private or restricted project usage needs admin visibility review before chargeback +- Actions: Show aggregate spend only; Suppress project title from department dashboard; Require admin approval before grant closeout export + +### usage-003 + +- Project: proj-carbon-sim +- Lab: lab-climate +- Grant: grant-nsf-climate-open +- Amount: $95.00 +- Status: approved +- Reasons: usage evidence, grant window, cost center, usage type, and owner all match +- Actions: Publish in admin dashboard allocation; Include in grant chargeback export packet + +### usage-004 + +- Project: proj-carbon-sim +- Lab: lab-climate +- Grant: grant-nsf-climate-open +- Amount: $310.00 +- Status: reallocate +- Reasons: grant does not permit this cost center or usage type +- Actions: Remove from grant spend until reallocated; Suggest a permitted lab cost center before dashboard publication + +### usage-005 + +- Project: proj-old-retina +- Lab: lab-neuro +- Grant: grant-nih-r01-vision +- Amount: $420.00 +- Status: hold +- Reasons: usage occurred outside the grant funding window +- Actions: Exclude from automated grant chargeback; Route to enterprise finance review + +### usage-006 + +- Project: proj-ocean-private +- Lab: lab-climate +- Grant: grant-nsf-climate-open +- Amount: $180.00 +- Status: private_review +- Reasons: private or restricted project usage needs admin visibility review before chargeback +- Actions: Show aggregate spend only; Suppress project title from department dashboard; Require admin approval before grant closeout export + +### usage-007 + +- Project: proj-vision-atlas +- Lab: lab-neuro +- Grant: grant-nih-r01-vision +- Amount: $80.00 +- Status: hold +- Reasons: usage record has no immutable job/export evidence +- Actions: Hold chargeback from dashboard totals; Request missing lab, grant, amount, or evidence mapping diff --git a/enterprise-usage-cost-allocation-guard/reports/summary.json b/enterprise-usage-cost-allocation-guard/reports/summary.json new file mode 100644 index 00000000..a05dfe0c --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/reports/summary.json @@ -0,0 +1,175 @@ +{ + "asOf": "2026-05-22T12:00:00.000Z", + "totals": { + "totalRecords": 7, + "totalAmountUsd": 1965, + "approvedAmountUsd": 715, + "heldAmountUsd": 940, + "reallocatedAmountUsd": 310, + "byStatus": { + "approved": 2, + "private_review": 2, + "reallocate": 1, + "hold": 2 + } + }, + "decisions": [ + { + "id": "usage-001", + "labId": "lab-neuro", + "projectId": "proj-vision-atlas", + "projectVisibility": "institutional", + "usageType": "compute", + "amountUsd": 620, + "costCenterId": "cc-neuro-grant", + "grantId": "grant-nih-r01-vision", + "ownerId": "owner-ada", + "approvedAmountUsd": 620, + "heldAmountUsd": 0, + "reallocatedAmountUsd": 0, + "reasons": [ + "usage evidence, grant window, cost center, usage type, and owner all match" + ], + "actions": [ + "Publish in admin dashboard allocation", + "Include in grant chargeback export packet" + ], + "status": "approved" + }, + { + "id": "usage-002", + "labId": "lab-neuro", + "projectId": "proj-retina-review", + "projectVisibility": "private", + "usageType": "ai_review", + "amountUsd": 260, + "costCenterId": "cc-neuro-grant", + "grantId": "grant-nih-r01-vision", + "ownerId": "owner-omar", + "approvedAmountUsd": 0, + "heldAmountUsd": 260, + "reallocatedAmountUsd": 0, + "reasons": [ + "private or restricted project usage needs admin visibility review before chargeback" + ], + "actions": [ + "Show aggregate spend only", + "Suppress project title from department dashboard", + "Require admin approval before grant closeout export" + ], + "status": "private_review" + }, + { + "id": "usage-003", + "labId": "lab-climate", + "projectId": "proj-carbon-sim", + "projectVisibility": "public", + "usageType": "compute", + "amountUsd": 95, + "costCenterId": "cc-climate-hpc", + "grantId": "grant-nsf-climate-open", + "ownerId": "owner-mei", + "approvedAmountUsd": 95, + "heldAmountUsd": 0, + "reallocatedAmountUsd": 0, + "reasons": [ + "usage evidence, grant window, cost center, usage type, and owner all match" + ], + "actions": [ + "Publish in admin dashboard allocation", + "Include in grant chargeback export packet" + ], + "status": "approved" + }, + { + "id": "usage-004", + "labId": "lab-climate", + "projectId": "proj-carbon-sim", + "projectVisibility": "public", + "usageType": "submission", + "amountUsd": 310, + "costCenterId": "cc-climate-core", + "grantId": "grant-nsf-climate-open", + "ownerId": "owner-mei", + "approvedAmountUsd": 0, + "heldAmountUsd": 0, + "reallocatedAmountUsd": 310, + "reasons": [ + "grant does not permit this cost center or usage type" + ], + "actions": [ + "Remove from grant spend until reallocated", + "Suggest a permitted lab cost center before dashboard publication" + ], + "status": "reallocate" + }, + { + "id": "usage-005", + "labId": "lab-neuro", + "projectId": "proj-old-retina", + "projectVisibility": "institutional", + "usageType": "storage", + "amountUsd": 420, + "costCenterId": "cc-neuro-grant", + "grantId": "grant-nih-r01-vision", + "ownerId": "owner-ada", + "approvedAmountUsd": 0, + "heldAmountUsd": 420, + "reallocatedAmountUsd": 0, + "reasons": [ + "usage occurred outside the grant funding window" + ], + "actions": [ + "Exclude from automated grant chargeback", + "Route to enterprise finance review" + ], + "status": "hold" + }, + { + "id": "usage-006", + "labId": "lab-climate", + "projectId": "proj-ocean-private", + "projectVisibility": "private", + "usageType": "storage", + "amountUsd": 180, + "costCenterId": "cc-climate-hpc", + "grantId": "grant-nsf-climate-open", + "ownerId": "owner-mei", + "approvedAmountUsd": 0, + "heldAmountUsd": 180, + "reallocatedAmountUsd": 0, + "reasons": [ + "private or restricted project usage needs admin visibility review before chargeback" + ], + "actions": [ + "Show aggregate spend only", + "Suppress project title from department dashboard", + "Require admin approval before grant closeout export" + ], + "status": "private_review" + }, + { + "id": "usage-007", + "labId": "lab-neuro", + "projectId": "proj-vision-atlas", + "projectVisibility": "public", + "usageType": "ai_review", + "amountUsd": 80, + "costCenterId": "cc-neuro-grant", + "grantId": "grant-nih-r01-vision", + "ownerId": "owner-ada", + "approvedAmountUsd": 0, + "heldAmountUsd": 80, + "reallocatedAmountUsd": 0, + "reasons": [ + "usage record has no immutable job/export evidence" + ], + "actions": [ + "Hold chargeback from dashboard totals", + "Request missing lab, grant, amount, or evidence mapping" + ], + "status": "hold" + } + ], + "auditDigest": "d5139452c6df2c02f396578ad66c0441c4a3238307cf314683161bba42b25da7" +} diff --git a/enterprise-usage-cost-allocation-guard/reports/summary.svg b/enterprise-usage-cost-allocation-guard/reports/summary.svg new file mode 100644 index 00000000..fce5f76b --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/reports/summary.svg @@ -0,0 +1,29 @@ + + + + + Enterprise Usage Cost Allocation Guard + Approved $715.00 / Held $940.00 / Reallocate $310.00 + approved + + 2 + hold + + 2 + private_review + + 2 + reallocate + + 1 + audit d5139452c6df2c02f396578ad66c0441c4a3238307cf3146 + diff --git a/enterprise-usage-cost-allocation-guard/requirements-map.md b/enterprise-usage-cost-allocation-guard/requirements-map.md new file mode 100644 index 00000000..4288327a --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/requirements-map.md @@ -0,0 +1,19 @@ +# Requirements Map + +| Issue #19 area | Coverage | +| --- | --- | +| Admin dashboards | Produces allocation, hold, reallocation, and private-review totals for institutional admins. | +| Usage stats | Evaluates storage, compute, submission, and AI-review spend records. | +| Compliance tracking | Blocks chargeback when grant windows, cost centers, owner sponsorship, or evidence are invalid. | +| Custom tags and internal initiatives | Preserves lab, grant, cost-center, project, and owner IDs for dashboard rollups. | +| Export pipelines | Emits reviewer packets suitable for grant closeout and chargeback export review. | +| API/webhooks | Outputs deterministic decisions and audit digests that can back future signed events. | + +## Non-overlap + +This is not a broad dashboard, export, webhook replay, compliance evidence, +identity provisioning, retention/legal hold, data residency, SLA, secret +rotation, quota, API-change, connector certification, incident, funder reporting, +AI-model governance, dashboard attribution, initiative-tag, policy-exception, +IRB, data-export approval, SCIM, deposit reconciliation, or admin notification +slice. It focuses specifically on usage cost allocation and chargeback safety. diff --git a/enterprise-usage-cost-allocation-guard/sample-data.js b/enterprise-usage-cost-allocation-guard/sample-data.js new file mode 100644 index 00000000..21c43b2a --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/sample-data.js @@ -0,0 +1,132 @@ +const sampleEnterpriseUsagePacket = { + asOf: "2026-05-22T12:00:00.000Z", + labs: [ + { + labId: "lab-neuro", + name: "Neuroimaging Methods Lab", + activeCostCenters: ["cc-neuro-core", "cc-neuro-grant"], + ownerIds: ["owner-ada", "owner-omar"], + }, + { + labId: "lab-climate", + name: "Climate Model Evaluation Unit", + activeCostCenters: ["cc-climate-core", "cc-climate-hpc"], + ownerIds: ["owner-mei"], + }, + ], + grants: [ + { + grantId: "grant-nih-r01-vision", + funder: "NIH", + startsAt: "2026-01-01T00:00:00.000Z", + endsAt: "2026-12-31T23:59:59.000Z", + allowedCostCenters: ["cc-neuro-grant"], + allowedUsageTypes: ["compute", "storage", "ai_review"], + }, + { + grantId: "grant-nsf-climate-open", + funder: "NSF", + startsAt: "2026-03-01T00:00:00.000Z", + endsAt: "2027-02-28T23:59:59.000Z", + allowedCostCenters: ["cc-climate-hpc"], + allowedUsageTypes: ["compute", "storage", "submission"], + }, + ], + usageRecords: [ + { + id: "usage-001", + labId: "lab-neuro", + projectId: "proj-vision-atlas", + projectVisibility: "institutional", + usageType: "compute", + amountUsd: 620, + costCenterId: "cc-neuro-grant", + grantId: "grant-nih-r01-vision", + ownerId: "owner-ada", + occurredAt: "2026-05-15T10:30:00.000Z", + evidenceIds: ["job-run-778", "budget-line-vision-02"], + }, + { + id: "usage-002", + labId: "lab-neuro", + projectId: "proj-retina-review", + projectVisibility: "private", + usageType: "ai_review", + amountUsd: 260, + costCenterId: "cc-neuro-grant", + grantId: "grant-nih-r01-vision", + ownerId: "owner-omar", + occurredAt: "2026-05-18T16:00:00.000Z", + evidenceIds: ["review-batch-443"], + privateDataClass: "human_subjects", + }, + { + id: "usage-003", + labId: "lab-climate", + projectId: "proj-carbon-sim", + projectVisibility: "public", + usageType: "compute", + amountUsd: 95, + costCenterId: "cc-climate-hpc", + grantId: "grant-nsf-climate-open", + ownerId: "owner-mei", + occurredAt: "2026-05-20T08:10:00.000Z", + evidenceIds: ["hpc-job-226", "repository-export-59"], + }, + { + id: "usage-004", + labId: "lab-climate", + projectId: "proj-carbon-sim", + projectVisibility: "public", + usageType: "submission", + amountUsd: 310, + costCenterId: "cc-climate-core", + grantId: "grant-nsf-climate-open", + ownerId: "owner-mei", + occurredAt: "2026-05-21T11:00:00.000Z", + evidenceIds: ["submission-export-104"], + }, + { + id: "usage-005", + labId: "lab-neuro", + projectId: "proj-old-retina", + projectVisibility: "institutional", + usageType: "storage", + amountUsd: 420, + costCenterId: "cc-neuro-grant", + grantId: "grant-nih-r01-vision", + ownerId: "owner-ada", + occurredAt: "2025-12-20T09:00:00.000Z", + evidenceIds: ["bucket-archive-91"], + }, + { + id: "usage-006", + labId: "lab-climate", + projectId: "proj-ocean-private", + projectVisibility: "private", + usageType: "storage", + amountUsd: 180, + costCenterId: "cc-climate-hpc", + grantId: "grant-nsf-climate-open", + ownerId: "owner-mei", + occurredAt: "2026-05-21T15:30:00.000Z", + evidenceIds: ["restricted-bucket-12"], + privateDataClass: "embargoed_partner_data", + }, + { + id: "usage-007", + labId: "lab-neuro", + projectId: "proj-vision-atlas", + projectVisibility: "public", + usageType: "ai_review", + amountUsd: 80, + costCenterId: "cc-neuro-grant", + grantId: "grant-nih-r01-vision", + ownerId: "owner-ada", + occurredAt: "2026-05-22T09:00:00.000Z", + evidenceIds: [], + }, + ], +}; + +module.exports = { sampleEnterpriseUsagePacket }; diff --git a/enterprise-usage-cost-allocation-guard/test.js b/enterprise-usage-cost-allocation-guard/test.js new file mode 100644 index 00000000..828932e6 --- /dev/null +++ b/enterprise-usage-cost-allocation-guard/test.js @@ -0,0 +1,46 @@ +const assert = require("node:assert/strict"); +const { + APPROVED, + HOLD, + PRIVATE_REVIEW, + REALLOCATE, + analyzeEnterpriseUsage, + createAuditDigest, + renderMarkdownReport, + renderSvgSummary, +} = require("./index"); +const { sampleEnterpriseUsagePacket } = require("./sample-data"); + +const result = analyzeEnterpriseUsage(sampleEnterpriseUsagePacket); + +assert.equal(result.totals.totalRecords, 7); +assert.equal(result.totals.totalAmountUsd, 1965); +assert.equal(result.totals.approvedAmountUsd, 715); +assert.equal(result.totals.heldAmountUsd, 940); +assert.equal(result.totals.reallocatedAmountUsd, 310); +assert.equal(result.totals.byStatus[APPROVED], 2); +assert.equal(result.totals.byStatus[HOLD], 2); +assert.equal(result.totals.byStatus[PRIVATE_REVIEW], 2); +assert.equal(result.totals.byStatus[REALLOCATE], 1); + +const outsideWindow = result.decisions.find((decision) => decision.id === "usage-005"); +assert.equal(outsideWindow.status, HOLD); +assert.match(outsideWindow.reasons.join(" "), /outside the grant funding window/); + +const privateUsage = result.decisions.find((decision) => decision.id === "usage-002"); +assert.equal(privateUsage.status, PRIVATE_REVIEW); +assert.match(privateUsage.actions.join(" "), /Suppress project title/); + +const reallocated = result.decisions.find((decision) => decision.id === "usage-004"); +assert.equal(reallocated.status, REALLOCATE); + +assert.equal(createAuditDigest(result), result.auditDigest); +assert.match(renderMarkdownReport(result), /Enterprise Usage Cost Allocation Guard/); +assert.match(renderSvgSummary(result), /Approved \$715.00/); + +assert.throws( + () => analyzeEnterpriseUsage({ labs: [], grants: [], usageRecords: [{}] }), + /missing required usage field: id/ +); + +console.log("enterprise usage cost allocation guard tests passed");