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 00000000..88fbd066 Binary files /dev/null and b/enterprise-usage-cost-allocation-guard/reports/demo.mp4 differ 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");