diff --git a/compute-budget-reservation-guard/README.md b/compute-budget-reservation-guard/README.md new file mode 100644 index 00000000..b04617b1 --- /dev/null +++ b/compute-budget-reservation-guard/README.md @@ -0,0 +1,18 @@ +# Compute Budget Reservation Guard + +This module adds a focused revenue infrastructure guard for AI compute budget reservations. + +It evaluates whether institution-sponsored GPU or AI compute reservations can be released, invoiced, or recognized as revenue by checking PI and finance approvals, grant restrictions, available budget, overrun authorization, restricted-data agreements, expired unused reservations, invoice evidence, and deferred revenue actions. + +## Run + +```sh +node compute-budget-reservation-guard/test.js +node compute-budget-reservation-guard/demo.js +``` + +The demo writes JSON and Markdown reviewer artifacts to `compute-budget-reservation-guard/reports/`. + +## Review Surface + +The implementation is dependency-free, uses synthetic data only, and does not call external APIs or read credentials. diff --git a/compute-budget-reservation-guard/acceptance-notes.md b/compute-budget-reservation-guard/acceptance-notes.md new file mode 100644 index 00000000..459a8491 --- /dev/null +++ b/compute-budget-reservation-guard/acceptance-notes.md @@ -0,0 +1,19 @@ +# Acceptance Notes + +## Validation + +- `node compute-budget-reservation-guard/test.js` +- `node compute-budget-reservation-guard/demo.js` +- `node --check compute-budget-reservation-guard/index.js` +- `node --check compute-budget-reservation-guard/test.js` +- `node --check compute-budget-reservation-guard/demo.js` +- `ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 compute-budget-reservation-guard/demo.mp4` + +## Acceptance Coverage + +- Clean reservations can be released while recognizing only completed usage. +- Compute overruns without approval hold revenue recognition. +- Grant restrictions block ineligible commercial AI workloads. +- Expired unused reservations produce finance actions that release budget. +- Restricted data requires DPA or data-use-agreement evidence. +- The output audit digest is deterministic for reviewer replay. diff --git a/compute-budget-reservation-guard/demo.js b/compute-budget-reservation-guard/demo.js new file mode 100644 index 00000000..5f238083 --- /dev/null +++ b/compute-budget-reservation-guard/demo.js @@ -0,0 +1,108 @@ +const fs = require("fs"); +const path = require("path"); +const { evaluateComputeBudgetReservations } = require("./index"); + +const outputDir = path.join(__dirname, "reports"); +fs.mkdirSync(outputDir, { recursive: true }); + +const packet = { + accountId: "northbridge-research-cloud", + now: "2026-06-01T12:00:00Z", + grants: [ + { + id: "grant-open-compute", + remainingBudget: 15000, + restrictions: [], + allowedOverrunPercent: 5, + }, + { + id: "grant-public-health", + remainingBudget: 6000, + restrictions: ["no-commercial-ai"], + allowedOverrunPercent: 0, + }, + ], + reservations: [ + { + id: "res-folding-forecast", + projectId: "project-protein-folding", + grantId: "grant-open-compute", + requestedUnits: 100, + unitPrice: 45, + actualUnits: 88, + expiresAt: "2026-06-12T00:00:00Z", + job: { status: "completed" }, + dataSensitivity: "standard", + approvals: [ + { role: "pi", approvedAt: "2026-05-28T09:15:00Z" }, + { role: "finance", approvedAt: "2026-05-28T11:30:00Z" }, + ], + invoice: { evidenceIds: ["usage-report", "order-form", "invoice-draft"] }, + }, + { + id: "res-commercial-eval", + projectId: "project-partner-evaluation", + grantId: "grant-public-health", + requestedUnits: 90, + unitPrice: 50, + actualUnits: 111, + expiresAt: "2026-06-08T00:00:00Z", + workloadType: "commercial-ai-validation", + dataSensitivity: "restricted", + evidenceIds: ["irb-approval"], + job: { status: "completed" }, + approvals: [{ role: "pi", approvedAt: "2026-05-29T15:00:00Z" }], + invoice: { evidenceIds: [] }, + }, + { + id: "res-unused-batch", + projectId: "project-materials-scan", + grantId: "grant-open-compute", + requestedBudget: 2800, + actualCost: 0, + expiresAt: "2026-05-20T00:00:00Z", + job: { status: "not-started" }, + approvals: [ + { role: "pi", approvedAt: "2026-05-01T09:00:00Z" }, + { role: "finance", approvedAt: "2026-05-01T09:30:00Z" }, + ], + }, + ], +}; + +const report = evaluateComputeBudgetReservations(packet); +const jsonPath = path.join(outputDir, "compute-budget-reservation-report.json"); +const markdownPath = path.join(outputDir, "compute-budget-reservation-report.md"); + +fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2)); +fs.writeFileSync( + markdownPath, + [ + "# Compute Budget Reservation Guard Demo", + "", + `Decision: ${report.decision}`, + `Audit digest: ${report.auditDigest}`, + "", + "## Reservation Decisions", + "", + ...report.reservations.map( + (reservation) => + `- ${reservation.reservationId}: ${reservation.decision}; recognized USD ${reservation.recognizedRevenue.toFixed(2)}; deferred USD ${reservation.deferredRevenue.toFixed(2)}`, + ), + "", + "## Finance Actions", + "", + ...report.financeActions.map((action) => `- ${action.type}: ${action.reservationId}`), + "", + "## Findings", + "", + ...report.reservations.flatMap((reservation) => + reservation.findings.map((finding) => `- ${reservation.reservationId}: ${finding.severity} ${finding.code} - ${finding.message}`), + ), + "", + ].join("\n"), +); + +console.log(`Wrote ${jsonPath}`); +console.log(`Wrote ${markdownPath}`); +console.log(`${report.decision}: ${report.counts.findings} finding(s), ${report.auditDigest}`); diff --git a/compute-budget-reservation-guard/demo.mp4 b/compute-budget-reservation-guard/demo.mp4 new file mode 100644 index 00000000..153b513e Binary files /dev/null and b/compute-budget-reservation-guard/demo.mp4 differ diff --git a/compute-budget-reservation-guard/demo.svg b/compute-budget-reservation-guard/demo.svg new file mode 100644 index 00000000..4fcbcdcf --- /dev/null +++ b/compute-budget-reservation-guard/demo.svg @@ -0,0 +1,27 @@ + + Compute Budget Reservation Guard Demo + A revenue infrastructure demo showing compute reservations flowing through approval, grant, overrun, and revenue recognition checks. + + + SCIBASE bounty demo artifact + Compute Budget Reservation Guard + Issue #20: revenue infrastructure and AI compute billing + + + CONTROL FLOW + 1. Check PI and finance approvals + 2. Enforce grant and data-use limits + 3. Hold overruns without approval + 4. Release unused reservations + 5. Split recognized and deferred revenue + $ node compute-budget-reservation-guard/test.js + tests passed: approvals, grant holds, overruns, + unused budget releases, deterministic digest + Reviewer artifacts + reports/compute-budget-reservation-report.json + reports/compute-budget-reservation-report.md + + + + Committed video demo + focused tests + acceptance notes + diff --git a/compute-budget-reservation-guard/index.js b/compute-budget-reservation-guard/index.js new file mode 100644 index 00000000..dac2b500 --- /dev/null +++ b/compute-budget-reservation-guard/index.js @@ -0,0 +1,257 @@ +const crypto = require("crypto"); + +function asArray(value) { + if (!value) return []; + return Array.isArray(value) ? value : [value]; +} + +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return crypto.createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function normalize(value) { + return String(value || "").trim().toLowerCase(); +} + +function parseDate(value) { + const time = Date.parse(value || ""); + return Number.isNaN(time) ? 0 : time; +} + +function toCents(value) { + const number = Number(value || 0); + return Math.round(number * 100); +} + +function fromCents(value) { + return Math.round(value) / 100; +} + +function moneyLabel(cents, currency) { + return `${currency || "USD"} ${fromCents(cents).toFixed(2)}`; +} + +function indexById(items) { + return asArray(items).reduce((acc, item) => { + if (item && item.id) acc[item.id] = item; + return acc; + }, {}); +} + +function addFinding(findings, severity, code, message, remediation) { + findings.push({ severity, code, message, remediation }); +} + +function hasApproval(reservation, role) { + return asArray(reservation.approvals).some((approval) => normalize(approval.role) === normalize(role) && approval.approvedAt); +} + +function computeRequestedCents(reservation) { + if (reservation.requestedBudget != null) return toCents(reservation.requestedBudget); + const units = Number(reservation.requestedUnits || 0); + const unitPrice = Number(reservation.unitPrice || reservation.unitCost || 0); + return toCents(units * unitPrice); +} + +function computeActualCents(reservation) { + if (reservation.actualCost != null) return toCents(reservation.actualCost); + const units = Number(reservation.actualUnits || reservation.usedUnits || 0); + const unitPrice = Number(reservation.unitPrice || reservation.unitCost || 0); + return toCents(units * unitPrice); +} + +function evaluateReservation(reservation, context) { + const now = context.now; + const currency = reservation.currency || context.currency || "USD"; + const grant = context.grants[reservation.grantId] || reservation.grant || {}; + const requestedCents = computeRequestedCents(reservation); + const actualCents = computeActualCents(reservation); + const capCents = toCents(reservation.budgetCap || grant.remainingBudget || grant.awardBalance || 0); + const allowedOverrunPercent = Number(reservation.allowedOverrunPercent ?? grant.allowedOverrunPercent ?? 0); + const findings = []; + const actions = []; + const evidenceIds = asArray(reservation.evidenceIds); + const invoiceEvidence = asArray(reservation.invoice && reservation.invoice.evidenceIds); + const expiresAt = parseDate(reservation.expiresAt); + const completed = normalize(reservation.job && reservation.job.status) === "completed"; + const restrictedWorkload = asArray(grant.restrictions).some((restriction) => { + const key = normalize(restriction); + return key === "no-commercial-ai" && normalize(reservation.workloadType).includes("commercial"); + }); + + if (!reservation.id || !reservation.projectId) { + addFinding( + findings, + "blocker", + "RESERVATION_CONTEXT_MISSING", + "Reservation id and project id are required before revenue or compute access can be evaluated.", + "Attach a stable reservation id and project id to the compute budget packet.", + ); + } + + if (!hasApproval(reservation, "pi")) { + addFinding( + findings, + "blocker", + "PI_APPROVAL_MISSING", + "The compute reservation is missing principal investigator approval.", + "Collect PI approval before accepting a sponsored AI compute reservation.", + ); + } + + if (!hasApproval(reservation, "finance")) { + addFinding( + findings, + "warning", + "FINANCE_APPROVAL_MISSING", + "Finance has not approved the reservation cap or charge route.", + "Route the reservation through finance approval before the job starts or invoices are created.", + ); + } + + if (restrictedWorkload) { + addFinding( + findings, + "blocker", + "GRANT_RESTRICTION_BLOCKS_WORKLOAD", + "The linked grant excludes commercial AI compute workloads.", + "Move the job to an eligible funding source or change the workload classification.", + ); + actions.push({ type: "hold-grant-restricted", reservationId: reservation.id, grantId: grant.id || reservation.grantId }); + } + + if (capCents > 0 && requestedCents > capCents) { + addFinding( + findings, + "blocker", + "REQUEST_EXCEEDS_AVAILABLE_BUDGET", + `${moneyLabel(requestedCents, currency)} requested exceeds the available cap ${moneyLabel(capCents, currency)}.`, + "Reduce the reservation or attach an approved budget increase before compute is released.", + ); + } + + if (actualCents > requestedCents && requestedCents > 0) { + const overrunPercent = ((actualCents - requestedCents) / requestedCents) * 100; + if (overrunPercent > allowedOverrunPercent && !hasApproval(reservation, "overrun")) { + addFinding( + findings, + "blocker", + "COMPUTE_OVERRUN_APPROVAL_MISSING", + `Actual usage is ${overrunPercent.toFixed(1)}% over the reserved budget without overrun approval.`, + "Pause billing and collect explicit overrun approval before recognizing the excess usage.", + ); + actions.push({ type: "require-overrun-approval", reservationId: reservation.id, overrunPercent: Number(overrunPercent.toFixed(2)) }); + } + } + + if (normalize(reservation.dataSensitivity) === "restricted" && !evidenceIds.includes("dpa") && !evidenceIds.includes("data-use-agreement")) { + addFinding( + findings, + "blocker", + "RESTRICTED_DATA_AGREEMENT_MISSING", + "Restricted data compute is missing DPA or data-use-agreement evidence.", + "Attach the signed data agreement before releasing GPUs or revenue recognition.", + ); + } + + if (expiresAt > 0 && expiresAt < now && actualCents === 0) { + addFinding( + findings, + "warning", + "UNUSED_RESERVATION_EXPIRED", + "The reservation expired without usage.", + "Release unused budget and keep an audit note for the funding ledger.", + ); + actions.push({ type: "release-unused-budget", reservationId: reservation.id, amount: fromCents(requestedCents), currency }); + } + + const unusedCents = Math.max(0, requestedCents - actualCents); + const unusedRatio = requestedCents > 0 ? unusedCents / requestedCents : 0; + if (completed && unusedRatio >= Number(reservation.unusedReleaseThreshold ?? 0.35)) { + actions.push({ type: "release-unused-budget", reservationId: reservation.id, amount: fromCents(unusedCents), currency }); + } + + if (completed && invoiceEvidence.length === 0) { + addFinding( + findings, + "warning", + "INVOICE_EVIDENCE_MISSING", + "The completed compute job has no invoice evidence packet.", + "Attach usage report, order form, and invoice artifacts before revenue is marked billable.", + ); + } + + const blockers = findings.filter((finding) => finding.severity === "blocker"); + const warnings = findings.filter((finding) => finding.severity === "warning"); + const recognizedCents = completed && blockers.length === 0 ? actualCents : 0; + const deferredCents = Math.max(0, requestedCents - recognizedCents); + + if (deferredCents > 0) { + actions.push({ type: "defer-revenue", reservationId: reservation.id, amount: fromCents(deferredCents), currency }); + } + + const review = { + reservationId: reservation.id, + projectId: reservation.projectId, + decision: blockers.length > 0 ? "hold-reservation" : warnings.length > 0 ? "approve-with-controls" : "approved", + requestedAmount: fromCents(requestedCents), + actualAmount: fromCents(actualCents), + recognizedRevenue: fromCents(recognizedCents), + deferredRevenue: fromCents(deferredCents), + currency, + findings, + financeActions: actions, + }; + + return { + ...review, + reservationDigest: digest(review), + }; +} + +function evaluateComputeBudgetReservations(packet = {}) { + const context = { + now: parseDate(packet.now || new Date().toISOString()), + currency: packet.currency || "USD", + grants: indexById(packet.grants), + }; + const reservations = asArray(packet.reservations).map((reservation) => evaluateReservation(reservation, context)); + const counts = reservations.reduce( + (acc, reservation) => { + acc[reservation.decision] = (acc[reservation.decision] || 0) + 1; + acc.findings += reservation.findings.length; + return acc; + }, + { approved: 0, "approve-with-controls": 0, "hold-reservation": 0, findings: 0 }, + ); + const financeActions = reservations.flatMap((reservation) => reservation.financeActions); + const report = { + accountId: packet.accountId || "unknown-account", + generatedAt: packet.now || new Date().toISOString(), + decision: counts["hold-reservation"] > 0 ? "hold-compute-release" : counts["approve-with-controls"] > 0 ? "release-with-controls" : "release-compute", + counts, + reservations, + financeActions, + }; + + return { + ...report, + auditDigest: digest(report), + }; +} + +module.exports = { + evaluateComputeBudgetReservations, + stableStringify, +}; diff --git a/compute-budget-reservation-guard/reports/compute-budget-reservation-report.json b/compute-budget-reservation-guard/reports/compute-budget-reservation-report.json new file mode 100644 index 00000000..18e7aae4 --- /dev/null +++ b/compute-budget-reservation-guard/reports/compute-budget-reservation-report.json @@ -0,0 +1,164 @@ +{ + "accountId": "northbridge-research-cloud", + "generatedAt": "2026-06-01T12:00:00Z", + "decision": "hold-compute-release", + "counts": { + "approved": 1, + "approve-with-controls": 1, + "hold-reservation": 1, + "findings": 6 + }, + "reservations": [ + { + "reservationId": "res-folding-forecast", + "projectId": "project-protein-folding", + "decision": "approved", + "requestedAmount": 4500, + "actualAmount": 3960, + "recognizedRevenue": 3960, + "deferredRevenue": 540, + "currency": "USD", + "findings": [], + "financeActions": [ + { + "type": "defer-revenue", + "reservationId": "res-folding-forecast", + "amount": 540, + "currency": "USD" + } + ], + "reservationDigest": "c4cd9fb5687f64355113d69bdda8d77c726f8c0209f1075760dbd9eb6f319212" + }, + { + "reservationId": "res-commercial-eval", + "projectId": "project-partner-evaluation", + "decision": "hold-reservation", + "requestedAmount": 4500, + "actualAmount": 5550, + "recognizedRevenue": 0, + "deferredRevenue": 4500, + "currency": "USD", + "findings": [ + { + "severity": "warning", + "code": "FINANCE_APPROVAL_MISSING", + "message": "Finance has not approved the reservation cap or charge route.", + "remediation": "Route the reservation through finance approval before the job starts or invoices are created." + }, + { + "severity": "blocker", + "code": "GRANT_RESTRICTION_BLOCKS_WORKLOAD", + "message": "The linked grant excludes commercial AI compute workloads.", + "remediation": "Move the job to an eligible funding source or change the workload classification." + }, + { + "severity": "blocker", + "code": "COMPUTE_OVERRUN_APPROVAL_MISSING", + "message": "Actual usage is 23.3% over the reserved budget without overrun approval.", + "remediation": "Pause billing and collect explicit overrun approval before recognizing the excess usage." + }, + { + "severity": "blocker", + "code": "RESTRICTED_DATA_AGREEMENT_MISSING", + "message": "Restricted data compute is missing DPA or data-use-agreement evidence.", + "remediation": "Attach the signed data agreement before releasing GPUs or revenue recognition." + }, + { + "severity": "warning", + "code": "INVOICE_EVIDENCE_MISSING", + "message": "The completed compute job has no invoice evidence packet.", + "remediation": "Attach usage report, order form, and invoice artifacts before revenue is marked billable." + } + ], + "financeActions": [ + { + "type": "hold-grant-restricted", + "reservationId": "res-commercial-eval", + "grantId": "grant-public-health" + }, + { + "type": "require-overrun-approval", + "reservationId": "res-commercial-eval", + "overrunPercent": 23.33 + }, + { + "type": "defer-revenue", + "reservationId": "res-commercial-eval", + "amount": 4500, + "currency": "USD" + } + ], + "reservationDigest": "323939fc7ea477f6a353a8de9ca4cb32bcd5a5062e91fc7ff3bda7662bf62d22" + }, + { + "reservationId": "res-unused-batch", + "projectId": "project-materials-scan", + "decision": "approve-with-controls", + "requestedAmount": 2800, + "actualAmount": 0, + "recognizedRevenue": 0, + "deferredRevenue": 2800, + "currency": "USD", + "findings": [ + { + "severity": "warning", + "code": "UNUSED_RESERVATION_EXPIRED", + "message": "The reservation expired without usage.", + "remediation": "Release unused budget and keep an audit note for the funding ledger." + } + ], + "financeActions": [ + { + "type": "release-unused-budget", + "reservationId": "res-unused-batch", + "amount": 2800, + "currency": "USD" + }, + { + "type": "defer-revenue", + "reservationId": "res-unused-batch", + "amount": 2800, + "currency": "USD" + } + ], + "reservationDigest": "6c969d8043f2a24b3aa48e7b71d4ecab978fa7ae200c229d7e85866802787d1b" + } + ], + "financeActions": [ + { + "type": "defer-revenue", + "reservationId": "res-folding-forecast", + "amount": 540, + "currency": "USD" + }, + { + "type": "hold-grant-restricted", + "reservationId": "res-commercial-eval", + "grantId": "grant-public-health" + }, + { + "type": "require-overrun-approval", + "reservationId": "res-commercial-eval", + "overrunPercent": 23.33 + }, + { + "type": "defer-revenue", + "reservationId": "res-commercial-eval", + "amount": 4500, + "currency": "USD" + }, + { + "type": "release-unused-budget", + "reservationId": "res-unused-batch", + "amount": 2800, + "currency": "USD" + }, + { + "type": "defer-revenue", + "reservationId": "res-unused-batch", + "amount": 2800, + "currency": "USD" + } + ], + "auditDigest": "3097df27e1490f943426267ef03818e3e7585940d329a47896daff2d4d19abc2" +} \ No newline at end of file diff --git a/compute-budget-reservation-guard/reports/compute-budget-reservation-report.md b/compute-budget-reservation-guard/reports/compute-budget-reservation-report.md new file mode 100644 index 00000000..1fb1b812 --- /dev/null +++ b/compute-budget-reservation-guard/reports/compute-budget-reservation-report.md @@ -0,0 +1,28 @@ +# Compute Budget Reservation Guard Demo + +Decision: hold-compute-release +Audit digest: 3097df27e1490f943426267ef03818e3e7585940d329a47896daff2d4d19abc2 + +## Reservation Decisions + +- res-folding-forecast: approved; recognized USD 3960.00; deferred USD 540.00 +- res-commercial-eval: hold-reservation; recognized USD 0.00; deferred USD 4500.00 +- res-unused-batch: approve-with-controls; recognized USD 0.00; deferred USD 2800.00 + +## Finance Actions + +- defer-revenue: res-folding-forecast +- hold-grant-restricted: res-commercial-eval +- require-overrun-approval: res-commercial-eval +- defer-revenue: res-commercial-eval +- release-unused-budget: res-unused-batch +- defer-revenue: res-unused-batch + +## Findings + +- res-commercial-eval: warning FINANCE_APPROVAL_MISSING - Finance has not approved the reservation cap or charge route. +- res-commercial-eval: blocker GRANT_RESTRICTION_BLOCKS_WORKLOAD - The linked grant excludes commercial AI compute workloads. +- res-commercial-eval: blocker COMPUTE_OVERRUN_APPROVAL_MISSING - Actual usage is 23.3% over the reserved budget without overrun approval. +- res-commercial-eval: blocker RESTRICTED_DATA_AGREEMENT_MISSING - Restricted data compute is missing DPA or data-use-agreement evidence. +- res-commercial-eval: warning INVOICE_EVIDENCE_MISSING - The completed compute job has no invoice evidence packet. +- res-unused-batch: warning UNUSED_RESERVATION_EXPIRED - The reservation expired without usage. diff --git a/compute-budget-reservation-guard/requirements-map.md b/compute-budget-reservation-guard/requirements-map.md new file mode 100644 index 00000000..009adc2d --- /dev/null +++ b/compute-budget-reservation-guard/requirements-map.md @@ -0,0 +1,15 @@ +# Requirements Map + +Issue #20 asks for high-value revenue infrastructure around payments, subscriptions, grants, billing, analytics, and AI compute. + +This submission focuses on a separate AI compute budget reservation lane: + +- Validates PI and finance approval before sponsored compute is released. +- Checks grant restrictions and available funding before usage becomes billable. +- Holds restricted-data workloads until DPA or data-use-agreement evidence is attached. +- Detects compute cost overruns and requires explicit overrun approval. +- Releases expired or unused budget back to the funding ledger. +- Produces recognized and deferred revenue amounts for completed reservations. +- Emits deterministic audit digests for billing review and bounty acceptance. + +The scope is intentionally narrow so it does not overlap with generic billing, payment-webhook, prepaid-credit, seat-roster, or usage-meter submissions. diff --git a/compute-budget-reservation-guard/test.js b/compute-budget-reservation-guard/test.js new file mode 100644 index 00000000..f5b40310 --- /dev/null +++ b/compute-budget-reservation-guard/test.js @@ -0,0 +1,133 @@ +const assert = require("assert"); +const { evaluateComputeBudgetReservations } = require("./index"); + +function basePacket(overrides = {}) { + return { + accountId: "inst-ai-lab", + now: "2026-06-01T12:00:00Z", + grants: [ + { + id: "grant-ai-open", + remainingBudget: 12000, + restrictions: [], + allowedOverrunPercent: 5, + }, + ], + reservations: [ + { + id: "res-gpu-101", + projectId: "project-protein-folding", + grantId: "grant-ai-open", + requestedUnits: 100, + unitPrice: 40, + actualUnits: 92, + currency: "USD", + expiresAt: "2026-06-10T00:00:00Z", + dataSensitivity: "standard", + job: { status: "completed" }, + approvals: [ + { role: "pi", approvedAt: "2026-05-30T09:00:00Z" }, + { role: "finance", approvedAt: "2026-05-30T10:00:00Z" }, + ], + invoice: { evidenceIds: ["usage-report", "order-form"] }, + }, + ], + ...overrides, + }; +} + +function testApprovedReservationRecognizesCompletedUsage() { + const result = evaluateComputeBudgetReservations(basePacket()); + + assert.equal(result.decision, "release-compute"); + assert.equal(result.counts.approved, 1); + assert.equal(result.reservations[0].recognizedRevenue, 3680); + assert.equal(result.reservations[0].deferredRevenue, 320); +} + +function testOverrunWithoutApprovalHoldsReservation() { + const result = evaluateComputeBudgetReservations( + basePacket({ + reservations: [ + { + ...basePacket().reservations[0], + requestedUnits: 100, + actualUnits: 118, + approvals: [{ role: "pi", approvedAt: "2026-05-30T09:00:00Z" }], + }, + ], + }), + ); + + assert.equal(result.decision, "hold-compute-release"); + assert.ok(result.reservations[0].findings.some((finding) => finding.code === "COMPUTE_OVERRUN_APPROVAL_MISSING")); + assert.ok(result.financeActions.some((action) => action.type === "require-overrun-approval")); +} + +function testGrantRestrictionBlocksCommercialAiWorkload() { + const result = evaluateComputeBudgetReservations( + basePacket({ + grants: [{ id: "grant-basic-research", remainingBudget: 9000, restrictions: ["no-commercial-ai"] }], + reservations: [ + { + ...basePacket().reservations[0], + grantId: "grant-basic-research", + workloadType: "commercial-ai-validation", + }, + ], + }), + ); + + assert.equal(result.reservations[0].decision, "hold-reservation"); + assert.ok(result.reservations[0].findings.some((finding) => finding.code === "GRANT_RESTRICTION_BLOCKS_WORKLOAD")); +} + +function testExpiredUnusedReservationReleasesBudget() { + const result = evaluateComputeBudgetReservations( + basePacket({ + reservations: [ + { + ...basePacket().reservations[0], + actualUnits: 0, + expiresAt: "2026-05-25T00:00:00Z", + job: { status: "not-started" }, + }, + ], + }), + ); + + assert.equal(result.decision, "release-with-controls"); + assert.ok(result.financeActions.some((action) => action.type === "release-unused-budget" && action.amount === 4000)); +} + +function testRestrictedDataRequiresAgreementEvidence() { + const result = evaluateComputeBudgetReservations( + basePacket({ + reservations: [ + { + ...basePacket().reservations[0], + dataSensitivity: "restricted", + evidenceIds: ["irb-approval"], + }, + ], + }), + ); + + assert.equal(result.decision, "hold-compute-release"); + assert.ok(result.reservations[0].findings.some((finding) => finding.code === "RESTRICTED_DATA_AGREEMENT_MISSING")); +} + +function testDeterministicDigest() { + const first = evaluateComputeBudgetReservations(basePacket()); + const second = evaluateComputeBudgetReservations(basePacket()); + assert.equal(first.auditDigest, second.auditDigest); +} + +testApprovedReservationRecognizesCompletedUsage(); +testOverrunWithoutApprovalHoldsReservation(); +testGrantRestrictionBlocksCommercialAiWorkload(); +testExpiredUnusedReservationReleasesBudget(); +testRestrictedDataRequiresAgreementEvidence(); +testDeterministicDigest(); + +console.log("compute-budget-reservation-guard tests passed");