From 7396e418726c1fb56bd4ac3e4662fa953ba0f9b0 Mon Sep 17 00:00:00 2001 From: HunterCML <5335527+HunterCML@users.noreply.github.com> Date: Fri, 22 May 2026 13:42:36 -0500 Subject: [PATCH] Add compute budget reservation guard --- compute-budget-reservation-guard/README.md | 18 ++ .../acceptance-notes.md | 19 ++ compute-budget-reservation-guard/demo.js | 108 ++++++++ compute-budget-reservation-guard/demo.mp4 | Bin 0 -> 39809 bytes compute-budget-reservation-guard/demo.svg | 27 ++ compute-budget-reservation-guard/index.js | 257 ++++++++++++++++++ .../compute-budget-reservation-report.json | 164 +++++++++++ .../compute-budget-reservation-report.md | 28 ++ .../requirements-map.md | 15 + compute-budget-reservation-guard/test.js | 133 +++++++++ 10 files changed, 769 insertions(+) create mode 100644 compute-budget-reservation-guard/README.md create mode 100644 compute-budget-reservation-guard/acceptance-notes.md create mode 100644 compute-budget-reservation-guard/demo.js create mode 100644 compute-budget-reservation-guard/demo.mp4 create mode 100644 compute-budget-reservation-guard/demo.svg create mode 100644 compute-budget-reservation-guard/index.js create mode 100644 compute-budget-reservation-guard/reports/compute-budget-reservation-report.json create mode 100644 compute-budget-reservation-guard/reports/compute-budget-reservation-report.md create mode 100644 compute-budget-reservation-guard/requirements-map.md create mode 100644 compute-budget-reservation-guard/test.js 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 0000000000000000000000000000000000000000..153b513e1d659e56af3b8c674bc3d4c0b489f93d GIT binary patch literal 39809 zcmeFXV|Zjuw=mqXolI;S6Wew&v2EL#V3LV#Ol;eBCYjjw#CE>U{X7Tnxz6wJ*IC!D zy}YVwRrRjb-5US^5Sh7nI#@c}+X4WffKLo;EQW5z%(f1!%m4tau&uql8vp>Xv30XB z0pkA#U`GG|S{MKX@Ok|g{67p({6BcX|FZnwC{O?Z#>U0T&>E=Jak2iVPq6=H`0r@I ze*Y)@kAD8Qej$K9pws_2l9`&gI0Gp>QyXWOe}@7__>2qw?=eH~*_v1y0(C^TCjWEp zyg(Z|z_)>aJn1b=Y@Gfb1K?(9V)`HSCsqUU_L7EnCN`#@HqbG)mUd=90^QB_pP>JJ z*kmUE(&3pnnSR=Q!lxeI)yanFUo^CWvx|`pknVPNasH15`LwD2Tr?p5g#RA%9|6SY zv?YP$XZr*Y3|XK(I|~B~Cj&DRGm(v@kp~Mq>%Wx$+_64BfS3(vB?4jsApF<{;Ftq( z#j>CxL^}vF000K~yn*#x1z{V20KhFSV#ePdA3uGO9SofvfcSG5pAFr}#p&NUKEeX> zpMd4;@=yLh=}(*F&;17kbl|i;*X0xb?GN5#{9m>}yH7cAjsC%~|Hl8pf3lzYe_{dg z|LOmgkN?Zhzw+?8J^pWh{NLx}|F7r6Kc721;4{tmpPl#5-uvgd_V4EyDDd=g_WHN( z69MI(R6sLBAWaEu6u?FfYlt7TiEJ&cuoJ>stfH+KBpcU}RQwa(2@lm=acHDWeEZLk)w?wo|dVcX) zeGt(T8QVLV5;3#05jlNjVfjkL#LCHJY{UdqNHPEo808ejBQQzILDV{1NUZYFLfB11bv8&79bJ|>T^+)N(K%wLIY zP5CTLJ&2rLjeryjk%O}*&A`7Y>;e=SJK6$S6H`MIFMB&+cxF0gW+F2~XBT}3 zXKPD`Pm6yAaCFeOH#2iKbpe{YSU3S4ocY*^Z0zl=4K09-{(s0=h@5RKje$e?4*?UA zozp*E7+cyJx_l19($2-y$;J?91e6=uxH=hn>KohJIv9S&HU?%0SdClS0Yd-{oeV#1 z%$y8uO`Z9^5*g_`cmiol6JR2Y^bJi69X=;vq;F(t_&F`$&N2OGTJENn<`ynSK%KpV zshz&Ly#rADFX88$tW7cYp$^f^o?!_SO5nL1kl z-JOi}|8u&0%%41;v6C5*EwI)!{hSt%;bZyAz(nNuIT=1C26iCn@EQ0YZ0Nzq&IJ@W zyO=ugu@YH20M`h(AAm~;?90#*xcr}s2?ziH+?-9rf&m;K3mya?okuKt3f^Tj;GqpK zf!x`t5xm2Uk=S88OR7;lpB3?cUSO{#OzarNvlO?$uNy!)#Fi`*=xl<0mxR4#xrOE-aMxd-x;Z~vE zAf=_0DloTvA`9&v9i!vjg&B_>2kV+z7OZw&N4(9R~dz(Nc_1w*m&p2!2pwA z1jlslo3oVdoL0^IwB4of(L#Y0+o9?IH3ziU$_92=1JPQ$?if^#WV`U~nS3Fa=N~dsqtY}r#;nns2o|ESH-qg-Vy@9&(PjB`~7JTv6Wr4Z(V;T_=3HBd7uCGc%otNU?N7)?p;U zBt6mzw@d99hQd7>XY;WE4(> zU0!i=)TXr+;Vu!<-UXOUS6wKJoPh*woT|UKwz0u+G#0G~yoIRNkUT(P(!zyUM1*W% zabi!2M(&2YL0CVAnwOFxyNVKCaP?;({pP)eTu{l1&>((&3^IPW2w#dcY++d-Y!DKf zSdu|sAW0Kw$BFt(-_>EW<)udN_vA9H;caEn9~;BaKI#$W9?})BYddN5naXL|un}sl zix{D|)0H=zh7AvErO7NabBJAD{G8$<^L`y@aQvn zR20Ey#1pe9!+6Ty)F1^NU2MSUy$`yo+SObPS6sdEL(aSM7h#jSZy3~lhRda%(xjA6 z-gqLj{4fHFDb%9Cxr{RZnKHIPu?{g4?)k+{s{uf#u9l6C2(`=thpsDOGMA0atW2n& z2&v{5r}8^)TAEoPKCT7H!9frTrcE%RkTJqbiFyS(Ky1}7*fN4G1M5pk8-*@SBtD`* z48fDZi_po7b&0d1TkJQtK#|jT%b<=S{tr80_h*pqH&W7k$c^2l97q1!!&KF2hfUUnC&0=+#$0kc&8uFzTq#Wdp#Ij^qpV=~vO|@I0gHH$%=kiu zv$yTQ=uq&T>b3^dwRm-cWu05-z%u13syMpifn2HlNd6ink|F{F>Tk~zY~^sEW!-_W zd$Bq}#q=f{G?-Jz)S&34i+; zZDkQHkP8>K77c<1rD8H;4y=j2`)i!o2KB`lNmNSFTYC_q9hp zwei#%6ZkP307e_DaXCAuz0)T>+E8CWGn5tUg#A*JO`&hw-H?TnsW4Szf>1 z7d)87lKk;B{vv1*0MMB&QmT9tVq@vu<8#0Q9GJ)Z>UjD_Y6or~!DeOZzhAcMb_bcM z5)Pxli%JMeKAmiCCRKxS;Cgtbd8~V(y8UqKe_i)b?=W~`()J?gpAjog{S(bDd!uYT zR7t#^CjS-ooM!w;XuAvLqL4^ROho7^&UrwHQ9j9v?qHCqL&w8Z1PVWwx~zM*71I{B z;4F8p{hR;{3?cZZe_02ktA#V&G~TMVCJ_u?Tj%S2<49KwpNl&qKgd|#VvOa2Jn2r> z3-_CO(E5UIGNAa#WJ9y+i)omwe&(=;_&O41CcxE12=o+HeE)A2_A8IeO6{+3b3Xps z#*wCxBFpbJE%oXOV}~~TyuV#03^9WaF{Km^I_fc#6+!x1rUFHUAYGDaT7>1NvSnBL zTPdgRemI*Cj&`ah$#c~_D6GzeYUqVGohTLHu9`WL_hpGCOUuq}cOV-{40VLmqzokr zGphIKrl(2c(kOeECqNk{Go z-_1vb#9$ZIr6KB@yUQ-egR)9H`gehchsbZrr!TCm?ly2*&%IPy5sDI==GkBbkQ#(0 zFDaFW*M^`q@sKe{QNrj`cb?FYUX z_je;zgDNj-7Ui<;bEk|s+BxZc9iP(aC8i2e)OjzK%&MBpWr~T1O_qGNJbtCW#!^Qq z8)nEUCMS<3r*4Dgzwg|!kMJRy>-2OlgujL_cz49}W1d9y&8!rJbk%rVPNYLdRkWWV z&!NDGE(zRnUXzFr$O-7Cj)p{{hn5*<6B@NyXEJZnZ6sC7uDU9+`aAORg9$!^`3tX{ zRv>=qUTe+FKzKFkH-&)JnW22hg{0abWf}dwxt0fv1l!6(7EK(cZP5HKHIuOH+^Yq} zXY746^b)>6ErqFaF3x~!*2h#RvG<~P9%Z)HR$2MjP7$fekGmI%^-4qpzP~WXU1hiq zecZnTq{fNtx*4lHSO)spY9j%FfmOO?2>m3GM;}rOzpDmLq)|yKE2+!Q7GI?2m;m^J z`M>bNtek@jZn4TPV^u4Hh1JYkYF`}X5w3960{vbUUVkL*n5u0zVl>_R+Ux0u{uJ-t zWeO>th+K+q9wk2vdH6-lFTThT$A53k@w73rxu@u(&GL)n(aE*X{PSEVNc z*067I6CvWi1yvi`n6e+(wssFgodIk)hV>KL1cT8_Q>#a&1>r1nl=zAw3K0f5mN$}F zH;dfYCytdyk*1dr%fvJ&9(}ulkR6CFC(iTrEG3CYv#2@!?lRn#YZT_=h(RoI-u{FG zB*9|`F;5pr>1rl1s6{I3@Hc0q?)S!LhN&w}>JxGUN}|T%gX;I!KV*5ly-PV4IW}zN zPej4aY)HEc?SnHaFhy5K5_kQ-xatThhFusX7e^6D(_G@T_&9Oi4*hr?M}w#1N423X zy@pRyXM0+rI%lliMHk_P;=OS+QmC0)gHT#4HAJz{tdvf2vfC&NZqL__RvWWM2dmk0 zGDYYNQk3WNzd@QVb_kav9l|5x{S9-GRv4QMtwFAmD8VC2A+nwMB@`Y#SWQ^7iyLmP zT{Rt$3L65yjW5$C4o_37tIOH(v+v|?3q36jWLSL6kM&_W9CL2}Ev+&q!zs4#Sh%QhlH-=LLF2{j-2&$Y(ecJn&*+$#8{yPD4x(x0d6Uv(yjV_MJAX+Wl zS%k4Ys2fSC%S+`d-GG4blmrqz5ur*P6jisXdon4LQzN7mj@`JhYmizPaj_iwn0neD zHQR_VY*R#BsooRhTN^_5t61X>VERp@G8A09zwrl=N|3c_BD_4I&UWsZvNS6L_&}U) z#gbCzXrnnQ;<1=gB!tE0=Js)vRnYgGwG|*Wl?|V%mGaZmKh6k|!yE!Z35-;^HdpkQ ze7z`ajCSd&3q%zAl&!p=hD5o|Zf}~xW^t!~(@l8-8cfhejJC0BU!&02`PUU-CY{L4 zKRzhkJ*PEV6dtw9#`JB*Dw*iMl5_im-fY=vv_^soWlibV645?7aUIrwdlujtG$W^7 zn5%|pM!9uy$;MiOBEqT%tH%);K}8dNA6M_FfM=1uk`9182Fd2Hpyx3LkN zIGVTM{NcN=9c~l#SuDoO&PKR7UsMy9zU2@bZfgpSa1hRBV|CvWmSV~sK-)g{Mh$5q z%Lyb@IJ9LlQ=?Jm;|@=qY#b+-wGm-UcE^nJWo_@-TXNgOHR(66_6EcpqS6f{s>M;Vk z((wdiKVzDO)aLhD*mwfUAm@|csMLk8d3&x94ssYhR=yzM?B=64-!H969ZzdO&%vln zA+E_0)f0XU;nKNQURTm)I`e_7y)QlnPx&sa%YOtfZEN=}=jLIEX4N8X{|;e|ZWw~q zwrp;pZ$5!H0RV`;Ei9&Mb1%gAHXU(*Mh|zxFsH3y^gJjaJ8)jSqHD4|M~C2xt2c}h zSdh1WX)(nC?H47iY@qJn|&oXlV8ChrS z%p0E7rqkUF<_#Z{z+4Ga7eV~T&(yW}BF#FY z!U*=4FOfj{0JLED40(CNiNUWQdJQ5-S6jO7RovVnjN(u`UXxAz$knr7K%Sp+e{O5* zS7JQkKO4aUQnE(2K}BQy)S(!gTUo!pqztY32%#;&I6L zduWp|hXj70WU5Js@>HO2v6fhR!m$?-M{uUyw-m>@o0R!}2O%9&{hR&}|iy8N>7Dq66yCkMzQ^=C}`UUq0t&akO)r~FDS!c`? z9D=nE`sL{@OyPuWVydkX9*I<^=UorP$Udqm6I~xgt06p1#dkv6dR!;KQkjbz=aEO9 zlp_O-XCJ^T?4YEI)e}1bm9lOM7DZk~kms3H_L?p{+`&HH@$O9LoeHNJnj!{c?nEQT z8!Y4-n#>~3p5Dc7D!pc8NaEACq+`5I&Nt|5Rxq2!i<45aGmOf9QwxT=g$zVFa?2(+m)5n5?O= zhC?~csdL{)#7wyDu8g{sXZjDNqZ}(3KedTVN}b{r`8~S7`WQawi+eFB9MtKXhvCH^ zBTmc5RIt~Nl7RSIB$yAtO!`CeQOcJAd9#S0aW+nQyi$bHzjC1`-erqAE{|>ToDUDy z3?|>Wv>)j@7R>?$3{IT3wZ(c${@T@K@Z*sxw=5B~EFG^KrV2-8!J6>tez6x}&fU>m z>aJnZZta(OIXTR5ncT!}j(^W+K*$-H%h*D`lh;7vH=c_7u9lcrjM#2YK(IUxvZg0=@XqfSMd~S=t46epaCr*U7GfB!_TN8L zKm!ax_NIrRYTHT_!@EfJo0+Bd9wr;$$zK%+>IK#i`H(toG(CWanuaK1iZu}>HLt|r ziyw1&iDD|1&SJy&MKE9R@7Q`|U4PNs_yh(ExG^s{8R2k}W6fgS%R2 zpb9J5>;g13SVa>Qtw}^I%G3O~oM$i8n@*6zN@KXxe)4%UR3j0f<#GgmQ+2Otg1Exf zek6#5s4V99$a0x{^GeP2o9@{09jIzmobJDJ^3RE&@GjS4yTw12o$z73qB-e7KjxM5_hU`1k-~fML)Kbb69OH-YVEf|Vrk*@7H9Z$!rr zp9cA&rFp{y)@|;ySM9EdOcK%Mg1>6=B@K^z>VAD4I(*FyJgv4ivD@aimwEo4k3M_Y zHsSORYV_cHlu};=nsKBbb0^;S7K-;O!G9~*twa>?T~yyRT30((@<|JP@cb~mze~nA z$Iln997EP>?3q8XnQtCX57mj46NkgWLJ5)h1_Qr4hcYih7o}|Z)nt{?4*ZzROrk#O zuX(`PB8vaRleYs2hN7BL*JjPE4^Fuuwvv&l)UC9fGm3aCrnOg9jnez zBS`6!e2sYD^J>-($TDD_o8D`(GkDe1pc2wAhhtR)58CyngbjKh;*ZbTh{gz8p4@FiKw{8eLV_QUGW`i$j2N6m* z#ltT8tYN5v9)ZFrW&~VEZr63s-mia}0`T;5>8c6SgYgrveAuPi;SY<}*H@`7QN&E1 zEh-cy6ijXx>~T#Ww+A3{;2T3J`B5Kaq2NMmE?*oo5}6=8|$Z;;}hSevEg z^&#LqMAvg?-%%0FKtmN>Z%^Iy%lNZdDn?ma`2N5Wh#uT;-@=Q( zt{r4xm}|$-gqLQLFpt9;z7D1^!s1(y!9+n1RfkiiObO(i*oL0%RuK9o@b$Xu&^*Or zp>Dl7JHxrB^s6NG5-!JJq0IAvo=8V4#L1cSm>rg(@3rdd zAUAp|BGLK{FhzYEC~d%`Gr!i3&3^H5m8-$xt(zNEdW);&hPB^xOJmWLKgEVk$SSTU z5)&D?V})cy_;KcVA9xa-9asse10sO5!M7IB@V1BRI$=W`C6@D7bs5iosVT3ttVZgq z^8V&%s}<>c@qqVpaEUw|hIh@NWzo@=QNIJdh1m-0P97_5_aJ8|uxpbu_--*$fz{CuBkwse$LfAr?-pJ)bkY(L2PD zyJT1TKr5d-^C3^&XVL#{?@DS@z(peP__|J~O7V(3DjaEXZloy@O|(uL_(kH`85Q6p zk`wm}Cy%4E`aEx|)1%MJIRKUH3KL@AO|yqXA0n&CVlD@pIHd}rFx`tVD9lgbyOi75 zVgF>-$-IZl51ydEMyYrOv$0=!z}Vsi36AhzFLgnc)a(S=L@{+&DlUFj?;2b}D}v|D8=YHl3ZBhcJW z0OV~&N7WKjbamg?1@CauBRcUdMa&1;a1!`>Mz$>tBQdG#qyhfte7x0q@0sZL=EzlY zb7@bx_AkxBq3Xd-xD^2(WoZYuK3#4Rv}@> z9SB5E)Y{rD%N;U-|BjJRDYCPxg&)(c;RVV^`g$ zTP-O1HPbZuEPmpaY+f0f0IRhJ?eFc3)y{28JceNjR_c@U6WG7v&iH%3-)3=i*k9L5 zQ$_po2h{$wf~XK77Fob^6GS*fEyT8V^r{On{xviX^YL*?^K~x)fCHrjbwS%@vCIMn2q3 zvg^L?wpr|TJ%-AM<%i~Y9F&^{c+m)S@~>oXunU`^C${_+CNEZt{Q4}&h0VMpSplzh z^`>iXW%O(gMT{3^JP0)K%E{*H1pzREvoYYw?tw~o%LDa%VI)DCBgxS5WvpyfWT&aVI0zuO& zcaqE8bBo(!L-#fD@7fl8|241v>KL^}H{rV8)b%RvZCx{ zKDWKee)H7MF-eljKDt9ZiMW!EwpyUn>-NSA1mJS-<*ydkb}cmgf!zM~c=MMh~nA77sHb;L%=a)-1pSWMwVwsSZeqc=IH znQVvbwCZpHHXuBM*`hIHONUt_n5nYX{47jCMZaKOu#!Ai0?}Z}ZKPD&!AK$vv|d~m zTCMBUCs?w>QJyw8W5FeI_HWMN75kDoe=h|9KqQ1uy2^#WELFcIgO$_x%S&G@y8lS( z`IVbNvjBU)8uEe1Z9a-qiV;`w!g8*lGmG+g8ZS&xLSH0(g;}E#6QL>VwkapA!FRZT zLC;0GN>4yr!c-mn(()p#qn>~8jJ_j4OcZ?^#HW`9HTLoB|Bb7q;~`OE<~y#Pg$HZts{9bogZB)yplt09<1l% zDD>uBmV#a)D%ZR6C9vc?>ITosXZY_P2o6;Xx%mSc!Aa=lB`8tRExt@@^1Ipk$k08x zj+%q45){xg?nv>>A%yC+CG6PQt#MKS5h}u*>~ILJF0CqPo2c?!zLmFk>fV8k-%7Q? zLS*XldlHW!oYO*r;s*>aS;e*=`~{LAAAGI)9%Hh9QyFz<=2Kw{a7bp5sc&?;zg>!G zQJ3`p7&o*1B88iuIh3BmGaxp{(Db_zWZw^ehf-}VyJSp!R^%b~iuRTfq(Wy@$Z8B9 z^R2?N-5J@PC2}1ejNo+Dg>;3`YkBW=WR_kx@e*b=9EmQtzCNtzAXn!P+-4E)FY8j6yOS=N+@taFF12@-nJ#V$AAz`9lUgT?P}LX2SdT1k|vUrSvr(tW?vuwyL<3 zq6=yFgYCl*3T23hQU`ISG4a#O9xn_6m=))**UK)Sgto?Z3=nt2B0~^PnD|zx54W6j z*!k3Sk6N(Y@B`0u7ve8?5j^6Qt{HO6;Rvl%`u)VF+$xZvYO=%pB&`v#TEdJM>Dh!a z-$R+P-CjDGkO!xouqep9;yJ<3K*ILLYP=B?(8i=fDq^wIy$sPBxV1VR1AnhdXr8n& zg6eM0!Rh!*99i6dNBR;kvrw>OVj7!p;+9V5y#CWn2$W{Eq^%KFN@YpIT14>H1b3#i zCi(sey$|fWO>&BC;2`&I&MIH+jQ_lIp#Y^Ml(&l*k)GXqgoXKKfx7kZ#u`gyu{2 z{2Fb66*B}Hi*v|4?X#!qYk#xvHpgSoqq0BZ==OjULjga3{Crb-n=<>e5Z#S!$+VZM z{~1#LBs1+W$o>SVvHyc)>U=)I_y>RHA=qGIsHL--;RzxWpa-HO4GICo6?3EQcL39F z14Q8XI*Dc`$Vz0;j!})hFNjaxUJnX4&fk(Jrr3GKG-1|xRucls%D&D!_nIU76#dDr zI=*s40w1GNJ{CH;UQS~yj=NIvY}Mapv_TD_c{^zQ^qd&fPrE3vf;+Ag3`w>htuHS9 z@0*v{7Qeu^@MEBhuaIs%^YC`vu&=i6{6R@n(SIJdq$ds+HXeBo3DW&o786r9&NT?Z zS$32JNG;lBIKnYP2KZx;g&NXK&zIKHXNcT|`U4nTr^8&NT(MuWDC+zB*lt*|h;~u{bC2Z5|gM3mYozdfx1AsBJ$9 zP_qD#1wA6r#UGwLA7!WfUcJ8@FTvV;z~hfcuY;y zJ&{1@OqSTvNvl%N{)P$}UXR;+EzkWEkNCFva5XsikAhM2=75lS(f4%^e{pyQF`43dI`?oQ_ z?U;CX`+r<37r$r9&MIUf?iLv;IyOM}YH~`<2~Wf=fcM^^4Tr|p$ZbRE-S9VD*GDGN zy61sgxZS!<-%yoM-AXMhVVVpYes{TC6`s912Nf$KM+V z#Z>~5-2QhRsqFkTD@sPQwL`*9316|M{x~(w(%Rj4V)&ENlhcgaoR<*t%YHawG+5eVYi^Bi{|stpq9 zSoPO|k2LQ{FM`E5S`tG{2f3w=66<~l4-2W?CiXOA8qJ@AVJ)W`R&+Wj{V4usOh;a1 z*E>SJ*Tf4bFZik*xh;+@^YBaBt%_tXS)UUc&GqlqIoxUJ{Ul@uyJ);s4yzhtUkcK| zax{qe@&F@eC8C*-yf!;q;>=eDH(c~TgSBfa+?f$*GTQ>L9s4ka3^B#=?CgB@OuJ%3%0x2ZA4jjyyr$w{=EGSj23oeGzES-lSq+!+77Xx zV<$7k5UdkMwbPm)UV(PQ1X2kCZm08;NkAu*1&QYrBL@_Ntw>%d=;`GmFO_k300pV=@KLmZi|>u?I!fFJ_Mx~@ufm&E8J>(e)nspzOjui zX*9dQ_wAm)32EeKgW&Xef>+6$kQU-WUZ;aLoAO_cXH4yy;vOGTXZt{p+&pgB21;;$ zK@yI;YZD%cIrdFoLxY&y>2&LNnX@G?CqaLyo{Gm~KrS(0nI^VN>hBcZsm+9M{gd-i zvle}yZ^WHqjwG4fl%P)0+P3#?V2!Xr;~7bafu|U-?v#)Uk6zssiKe$i<2Z}$8VVA0 z)0On{B@~G|R}WJC1i{x-t;!~4fqZ78KmZg&Iy*S%4Te;-d2EO}a3HAQBg^t=A>8j9 zezCbEnu=hmb;lzdnmfu}xs{fB+CkxBay@szT-igGGi|6ru_`So$7+AJ?hYB2VB`kE zNc_yg*Fy;IdARB|VbULkyRPHY)08?rz1^qdDriw*tUl@WJx3?VADPkYac`68OQcn6 zlw%y}ejA3jKDnfSVSwM@q;FJJXlI#0uuqgGDI_h%H<&|Brt+6do?j1`R>~mzno#31?)|*29k8+sL-JS$TT}$l~1tK+02)s~|xH$Q_5nJOJ=dV9eLDZ=0BmG~zqL zu}m=?R6JL+o=EI8Q7CScI-$6t!(odxZuz(jSyS}RguUMNHO=x?94#@#?yNnQCBs2v z7tHUS2HYRx$ZS-E(kB&u7dUq^%tsWO22bp@4hah}&-$b?jDBR<6hHrFW4kSl{S+oa zsI&b=zm=IM&x$(R`a|K^u+r12Co(O-kS{Y@#e6Y32rzvF?SP^M(ZA+hPj!NA{Y8c1 z0VQoD%Rn!R zvwF7&{madh=s}xDB9^pf$?$hrJr*WhTkCn<_IxBy4e3OedwpZRu&7Z}ESK4Ww8DWx zhA*(m9G>g?oP(`H*xIb(pyCySb!;8;KcE!YnXu^Bx{QC9!i?+T&3)%3)@iiI8Sj!< z@wM5Sb6*YEU6riInS@mQ3Xh|HrfN%ihDsG(Y0Q8jOZSr+9(EC@Ajt!jqb7U`)9&C$ zvrIzi)?_Wpktsx$&6pK3-ih4rpA-t=p@mHxO*YA7+s-t7hMbR6^bQU2+f{T{X5rmlX1?3F4J$+uk2X)@DG3p8*k)v)R_>!zU zr$C$BptY*Y_x$;;0nlG|5=-8kovh?wzqq0JqV$o5u=5Z2?%d;Q!|M2X3nmE9sah>F zuAxOU%InDr$cmnctJRi+!fC$oWv>*rC_H_?-Bj;>si)=|e!N?+I?$UNb|h`>wzszH zos@miH|b97~y>)>DJUbS$zqG=D>>`yUiR4#efSMS^H&sIEXw( zBFDBrs^zfg&a1hexI#wa4#O6U9qW9I+~TVS`Sl5$LYnp=tx z3?gKDxGNFMBG*Lwbt=_DsXGL}a*8;WwqT@nv{0`jPmTOL5;``hKxD0o#7Agq!-e%M zOV|6FkZ+xgD2JgGq>H!tcWpv{mcD4S6DXf0Y8g=LP;CF7G#^|MG_>-OHt>8w>vi`! zE$+1}f}QjcaT#{g+5UpO1NdEo({DjEVRP+obyQ?SH`P&ZKWzq?>qG0GLz^E5+j{c0 zs#b|!K&5NK6|LBg>Pcg#uMf@%?(dON9DgTQ!+B4<5WVNIsd;8rSrzR4KpD)Nhkm3_z*uQGlH$gg+9k}_xa_fY9AE4>+$lb)v8upse zmzCG5wNZ?qXARAObUV96g4rT71=?S$HF;|3~ z#k&5U`Be?<=6F>=t#vM~Ex3e0;#MTQv!eG-?bDlh`leV$q8XC4#C_ATPEHd zKmEAMNpJ6*mCs+GvDEPEJF*rNWygp8x;rQM?qb7r?~%$~R~{E-QIXD+jV4|IEFdGP zs$}v}L@ODAMvxCF_B|)ZtmG{lQnT`5aK{0$x7mHKhwpOvX4UqCCTd`%T6q>gf1+0k zzKa2}76dc3sp6D6t;#tAkv~6pftyMMFdY8Vh-DM5*|-=V%pNDLIYKYR4AWScHWo*= z@_TRd=XGKBH}o7tQ3W0xWH%)&&YT!YY_fO9?YS0)$}rF!#c^l3()8hWSZa<-URsK_^>?M1{*YguSDHesfdUYt|8`Ye8 z9AY`JH|pWJzK7!$cpd@bM!II94B7WLD@R9BL*m-H3;r0>`Q7H|9b_sLD*Z(AY68XT zi~ZS95$j(mMq%nJ^cruE2tI9Z1A^{6w0;yT_wZNFSex8w48-s9Y!Raz$4GSa+AHjQ zB6i}dJX=8OL~vfL3wyT|tJMC88&L;0o45Uqirw)9XM3DA@rr5tX!pJ9<%{&s@BMVJ zxun9m{T$_9Vd;1Y)q~^$KS$0(%gD5x`Rv>YM`+#5q@=T1ly35qm28I_ACt!j1%6m= zLC<39QpyIYIjxHz=~Fpt8(xnrYX4fR)z(l1#b`})!1{xfq1jG#Bg#u+s_(`R`ZU_0 zyx>55fnnc@;z8#lLV&8N(!yvsw7981xla#MRRBG()~A)BgHJ(t&ap*exrPeJ_Y+_Ns4vW0w|{ zhOvbxDZe#-iKQ<5jn7z*gsQNKHvC>=Oq2o*T0FQ$-}|BIMK2`D2os$k>oDpO^~WAx zHe#|y9ox0_{_n!yxJRM5{wlTKR_^p4_+dPq& zRhe`b7E3z}70LI!GNuGvE}8^-*Pc}8jnf=K-=Jc%_2&5&Q_179tY@)0Iz?+SUT!na0UNlFw_A!x|$2Y#JaPSRf()z`e#i-&(aib9n%Tsa z=S=N>6NP&OlYP52RF1J!UjC%%G&Vlxl8%!eUirN!HSa80*j%Ie@wPAI?=2+4xEYTb zds9Z&Z>W%6fbtJ664V&5ZIAi`M`+Dk$w#xV;s_mkOANnmiqL)496fWq%B_&@Wj71j zDy*?L?Kl6j_({qozI%dK?3ZeGojTp|_N-N62q>lBYos4UNcP`vXa#+sf-hSy5D;YF z`)Wm1qxgT5@*`aNX_WXVM|5^3^xVY}Q_3i1#`8OiWF7VU=v}{1f0=PF{tMhB-!&s= z!VoGc9|9)uM>1b%vrV3Z=M+VPAoi~(`bq5pX=z*G*n*1RL27E+KOU)V(ZIBhMP3uJ zJ;}H|(|>NA>NBiN=!?bs$%RDz7VjF*C>^|i1MDfU69lxq%u#2aGU6rvTCS5{oq)mW z_BeA;)afvQ6F1=M*lG4BS^*7-&K^puDcf$VPSBK|8$}HWh|nYck_b21qlJP2cBz`% zNx8Cnc3C9gd}Zpdt8U*h!$nwXTIq08%ASM;GR?`#(FH!2_D56Kq$*Vy-9lds54nG} zu4dQp0=W?pt;{vgg#EG@*RBd@Y^X0TjNGiZMW}#(YHF?Csz^g;0aP3W*^k(3t?pTp zbJGgM_+q*JIytA{7fYW93~^K0Sd)p5OL2Bi;_x)?P=wde1P2sv%8@>om^%G-@Kb_z z%yw*q&FHm>ezM0HYCXAQ%47;Qe)V^DNhI-a)qhj(MG>C4Hc+sh3A}P@VK1zf`=D4r z*aMI0G5Fw`huegs#>EP{qfm1H()!D%be!=RT*rY_!1p_Z3NZ)CY4jmPpTicV1j0Q{ zKn}n;Q7d{*O_B{iRxi@fFT`%s1b7Uo0$)d6XFPSwRJCL9o&r6oMm8}CIlHDISpA^Z zpHq0?#SXQqIuS}lfZjOO0{UPeQOq_(t^h$D9$a~Eq zJktNj2>x21?Y}LdJ}MfidAD^#N*Y@uWaHel)y(7lUOmRT(Ts{m33EbnTMF)9Bh_NN z2)QeIH(}0cwTi_9kAEoVo9k$=zy2E%K9Hw+b0Ed4X1$jxQZ!DES%@Bs9GoTcqqumH zeOTG|Z$y`3|KjY`D>M>aBM2APPxr~)2|S;*ex<&4t((!S<3`X6xIYA8sx`;=Y%RrI zUXKuZG#?b*hJ|Gr<%Rx7yZUJ|iDA45)MCn;twsC+VhE*whhlB4bLdi&Rp{E6%L%4j z)BdRKNK_F#azZ#U%ey{|xra^Yc|!XM{sfSz*}Q~Q2!#w5w8Yuw z5hu}JpHYb1;_#+;B4RM_uDUZ}MhTZmFevoxCA9PaJPutJ>x~l;^t%rGh#-O_uY+yp?5*9)Taf!w5r9 zkfymQcsd4WQG!c2b-VRnQr@~rl?p6RP59f~g2G}H_HuFE<6_@T(;Ye!qdJJZFuIYH zt=+oWd}V9spmyd5ZqiGk(c76pJ3tKKd5}q?QFN9yRh2L73| zGZCHV5{hi%`VDEnXkFR!M#3oXRi7Pz-tp3Z6tMA+cC1Y6aXtyw%v&hNC)~ z$Qry5x3abQt+JSAVp~#GX(LKMlJy5JCwG|4QVU_a==eO^Zum29alxx}dAe?zYFB>- zY5m17q$}~Y-NH}U!sT$_gx?`_AniYBz^%_4V3;fr@^vmuLiTw_AsqAcH^gAAOQC2m zK=DfRYpJzt5eJ)h{t^QJ4Sf7o4tk0wZAA%P(>k`(e*mz3a&NGu$fev4Ebucx}U|dMh6;tNJ^PMU=|jvJ2QG1=&?xwmZ*$ zwob?+N)`&@TGAlnQN$W{wO74B49A<6NOHAbfC&iFkESsaWDbh6d`OD3t$XuwOO zNrLGQgI7y*xg_Mk*cin(fXR`_wF@Cvt+bMw0`)S+l#r!euu^0C7Suy0HdAIGM7Fmq z!2z*Zu0`~uHF{no35q$hrpD>A`y(!6Rh-#{TSKPd6@Km;)HesoglHMO?1{+6!BlU{ zWbEbaZ1QM7Ix#zrKjn6nT5xY+22lj7zBECCzdH;hmo#CQao~j$4V{)jJjVigi<6$X zzHTRkkYCPC1I%mn1HEC1J(3nm%qx~s_{o}_NWsC0>7Fs|ggWDKu7=Uu*H7;@Lf5#| znj5Vrph{PJm4^%KpWfL+Wu!ySI!)WJ`{AC`gkQSvsovEZx4-L0OUi-Yg-heU(!_U1 zwA4T|RimGJiu_rNQf}7(Awdbb6d*fQfq`~mb35I7A8`T~)6RA%sf5X=i>5Bez!ffp zCc9h}sJoF<#0~0BaEN{z?wOQc=FTPbhpEBbd>r}z+Iy?0$hIY6Hxn(~-QC^Yp>Qvt za46i}ox79{dAx982OMRGscW>#>^QjB66*me<;p( z6EjAm(SHU$(9{b-(h;ZzEcEAl-=KwjPgkYa5v1ih`jh6Mr6nRCs#q~2j3o#kiqs&s z5cx|ITAZAnJLeI=8o@%y>dxWOXXJr|W)ilaX@v-H(l3Ml!**b6d35wAOM`=zNF?2528b7sCuI19KGzenTOV>2D+~q&4M>D~9DXrWKYKbL| zu--}g`F{%D#5v-N&YjS#ElD*SxV~reT!x(K8u*G2H4RR>OZZLOn3ViZm7vJ-&h4OQ zR=cFLrvF}#lfgARl&-_gxkPPaPzx(;{S~C8Rpz7c;K>MOG0*dDHJ?V~yY$QfaXyql z(K)>e;u8~bo8=F_rC$e7#$ok0o;}VFTKP-D9;p6BYh$8?u;bfGE>pcvS>nDe6-vRkM!u8r zjRv^iYd#~aXgtzc0CHjr~$AAi@p&q)@^Rywp&Yr)}~Kg;)3`r`^w^PTp1(L~TDCl-A#{u;Dj&fk@2 z6u`}bM#lCKon?}MEh$YwHekLzIj)Yi-({`iSUvHSvM4N=f%6gghwIK{{U*!+u>WkH z<;*clozR|#>y?7y-E25ykux02Jr7tgVDY&nlNQY--*piqHGWl8{ZhA3F(DsP%BC`%RwjuD`W+7saN) zLh(GhU~Cz)v!Mb`t;S#oNY~VD;|+^cAce3H0cnQCVflq+TW6c(iQwT$%QfocASz*~O zg1W?2k2{{>Vn8}NED9>k z@yPO4$?y!P+d{niLqcWKh))B1=7;SUIz_OP?P3AzhnZCpuH~%a#0${}4?b6ZagJ z!Wc$2HeL`{wdw}e->sO_S-k5K@$xXQGwz_QXVD2T|)0l*%@h5(uy#`G?2N80}}~V|(%`10)ktzXXk8P4ohDM(x033)MIT zx|s+LYvrhN%Az(J#Y|9@wjhZiExOtwgEu~l+FCNdoi}myqJ?6+*%&oDW(!6*H?HKC z>!Nn+ox~)%-dJOyUx8t{ms3~xk~$nb{@C+ah`9H2=!Hh>(UjFrfs})o&H3%^voQDD zhj2};g*Cw>+<(zfk6(6#ky47J&^Qii4BP?1}7}DlEc$b)1P@;`ugo z7rWM-;ZC$fcU(XoqPH8a^?@+i9tOzT6(N;37m$hreTgIFjvhx@6|H)l?ib9aQAOuR zd6*JfECjvz#?k*-oZA0w$B=R2rDcQxh&=5Cp#w?na1<^j#B2=HcSa_+N6F%nhm$hL zNR#@mE5dQ#L~-N7rW!8i^ap`25O)LM;Vj4A0h(>jG$x{*9=16O@+=ICO$@$Q@Zh$G zIrY-hMLPcKnejyI>NJ*dlwe6(D*_m6>03W!ANw-aNVNPWVbqAiEN^ zIfl^T8gvUFg}Bv|rihO!8?LcNu!xm;)`5MZyylK>Z z;*gqIH~Egm4oxuKNe#kPpB7Zw7+(jGWZ>FebXof~f$>HfsUXk?hHq8_q&@APqjz1l z&1lHy&yx>64NKv@B*Eg-xAcoTYaykOwuTo{XE!vYHz)A5Ko5q3ZM_b@t{9)z(ua&Q zf4o&r*xs65ZdjdyNNwkLX6W%yTNd$Xv{W;a3xE>S!9*3MRu3ZW4kuxJooYo_X4lfe+v-^l2Lb@A9}lys zLF4w6gtxN++W_vvs@gDJwWzv;#=&nP;M>KwMASivC?+pi=0O(6+{?xmDpYH%bd=w^ z7%T;2gNT}Z38P4sogPF&fe|yQY&{aN-9H2k;ReoMyW>$2n+r3gIpJ=|T})7p+}qel z?sv_Jcc$S7yp%iGKwqT2Uvx-?yTl`#i;gd@bY(GDclr^p9WnAn!B!H>`Zc{Uz?t_2 z+Za9gpd=>zM!mlM^Pog39X2c94`o@=4Lf63rOVxC}K@^21}NKD}w zv&HQkVTW*Lbm~uQ=1j^Yo$?_m*t#D`^W}=<8gc9vOU5Jtg)P+=@79OP!GfiZ<4RVV zST6tIIoDyjaKTX;U+U*2yTIWzzz*>227#vLvv+@-W;{VXG%SR8@lK>QNNb<|Vvbz5 zymo&xsx{fF>8Uv3oXhR2or_ z)Ps^Li(*wv)Vnd~%qLU3yKSV8#=Q9<4JWfxZKGzfjs`uig}(>vLZ?|3*ot9;nVN_s*UyYEs2S&0l80*B8C$6 z{{Fa!(cw&ZU{LlFF?CHhi}L4c0HVuNFw6V=+1I6JI3n!xv-y;k7km|p8q=wuDe)`a zZ;^WmeP8367Orhy4EIxd>^XH;Z1AVz_VIp_-4cQ5P6h*$S7X@4uNdQ<>RJ8-WEOs3 zH@Z=#G=ixw4bSBwx&IZWWH6P$LM&I5VNonrz6UI(HE@Y#@onuBKsbNHTk1)((GEgR z*Sym)$j%6dXQN+2N_+BFL5J2ho^>g=VrN^{kvozFO-llkGQ}omOI`M>2`8=H*6jnk z)4!p(FaeKJrho@f1D%nlzXa=o>7NA|`+R_n{XCE9B1XljoElWG(P<`PwLVXV&B?gP zwDL+8 zsEGyPh1YSAS8%WD_vAy#*p1rE;Ns@1Pya=h+AXuHS{qbp$xoH$wIz9zkc=`oULdx3 zrtXu~7yQ+H0VV*!v_fB)tcKK+p&G*#!ZD2eFbBWVEQ<-1g#>`A88{UAUW7O z*I>kOk-=eIPp5}*DX_<2yVrSqB8o73?a-C+0k`LR^dTc@aSlq9p2(oE%nV~&fEA=a zPuw?h)hjZaQG>4ZrI=-oObl0R*5TL zx9mm0oW!H1i*kSaNY*e*4Y@;(w;tulw<=1pUEChmfg%L4BTabDwkq>TH&;ffF!?Y- zkZQ3~Z~b%ZT5QalD1OtWuEKv=E6Bt>6@9;AHLkrac4xn#~>D;786@ z=drV9pmVjWbbg*h zs=#WHfIR@tsHPb>)v13-#F0Yt)z2{~L`4oa4=T;p*r`w1Ng`;gW7HVF-}!mB{V+_> zyI+~>SpR)~C(~4^UHA*|jV@upaUq6?7bc$6Y~s;O32>bipT~7!JL4cDqgmKSER?5& zq6G6h>GW$EdH|c4s{Jmy$5vwN)$LX@ckKRx=p6OKl|y-~-O)y&9#tZrMi*s=jN4Y* zLUL>VJPuO9EhHI}c_Q7yM41gY`y{Nma`8~*G*yI57bpX{dhku^t=*0*m5O00KR-y4-SsLihQ6^pPNoCAKfp&+*>~zH5-pi^Q%G zHk2Xyy^DIUuzZ2xj=Q}@baxM@_<~9=84z#&k#Sud>u$L)6BgIr-&~bWBIl!o)~wF)qrtth=w%YJXrexc2?0Iau`O zWC``*F^(mJp5Z7;t>!Y7r)za|TELsmqgK2+8g(iTr%sN=kerKuXRfzEZEo60RJSwZ zI)(IU!A@ecmG$5_?J=?ua>kBy!BAnieNXZhvH3+lz0|k;Ku>W2r0p%$v}v=)Fa_YNDDf{uxcTaf88X6w9-QsR1JQj#oY zaR^Vf zpV^C)U$W2DZ2ETLkIB;1=~nk(Q6mMfF?6r!Z|z8(hao}p5>deHU%(4Cb(xj61$|@q zgHI8#>{vzovtUmxf5h@E6mJn$#UH8P<+2L(gGkoS1TC$db*e~^5%!*aQF{ZNN#KJ%sXSM9-2ZVVSqlP%sT zc+`!F@LFW#Ovq)Ytrr#zr^B;_we&DHH)|Pa>i?d~z%e|=$L}i}B(NNdjZ`=NU?Zsd zn7#o{7*U^2M~aa0F^6j2;!nlaJ782dwYG~D-Oqbot5OeoQQT!mq$~+U{dAs(~oz3nS!Iwc72P6 zxw#wUh>K(3C|kSazt^;44tJy`_fZ^qD!wPz7{V;9w(8D;3#X&v&ir%g0 z$>NW5PwWh0@ovwE>PR;hwb!~sD9%Y!y|{V_vcCPoJlc3j9}0IaYFMIUCri0XOi~Lh zH)kp!sgRCtyemw){nd~`4x>C;34Z++-2g6j-Pg%p+;5;8Ef(2|ydEv+@r^>_R>jfWTq+)I7LbMJnOJG`f@@Xa~lQRU*D5M#Y!DK_O4lK1~D)aBHVXbQY+VQ%i zP~4r#eti&^j9jo2YB0zeP~2-Y2e^p|Odm`AR6Pnv74eoH#f7D?HbLG@`=5TTcDGe~ z-tvR0QBrOOrMO$e5$Jcr0+lVWfIG`6xy;L?0-(U3w%^^kp%+(&+7N~2`7rjyfBrnO zxVHAmaS3w_n>25MKx6FTMDq6@S;0pI%GuxTI=!n^dy`c?zKBKN$36b)wgOiP#Ko(t z!F@hU`sO77zTv9;xTP{r)(5wLd&#b&g0W)T-`88m@@Fj0!4zy1t9<-4y(OF~QvoKL0>b3DIh ziEqy7j~7nV%--Ku<$|>}12hD=mC~Zwr^EH{hEDrbH`fbs^U<+Oq`8<=li8N3n+4q2 zn}s4+_BC#3rm$KjWu0d1WLr%)$e&-!OEk@MEZ4^KI7=9ZUz7FN^q0gv zs^KBkVy4=}qt&f&UkHB%6A`Dn79>yhW&x7ysy*}IxgJOyN~{#Kgp&xszxsI((8gPT zFCy*AREe9rdOcn6(=QU?`Kb?F&@4TdG$w{fe;&{<35nd}9zgd8r0;Guv(xd^Q$>x= zEi5K?U>1I36I;See457*4a1}aG4A@f5v`JdqB&mdEj25F{A}Kb*uJtX`5gu$;yVHp z9y~g>SoPB{V!Gx+XqvG$pqBpLFXwPz-^ydiU?TIS#e^qjjOELTWSz*Ad^M5WK()=~p7!(g5}Gt!x)WgP|XtT&%J+ z0!#!OBGOsZ>7*AjRL7mEx34sUbEs_ldmhQpao+V`cx?i1F6O>v0#7!)Gu6G$dW!an zKI^}U@k7fSYO1Usd?y%I@327tpgOdeL)lFZTlk#=?(5)9-M^2escKeNn%vdZ|nCDP|8*BA)Do$TpuNICYU{@J}Nd>mGei79W!!I z{_XuVx^87*fVnH>rf(@`^23sRVdP*eX2ychOWf@q)>Wmf*Lhs&X6>Mcr}=w`k?yp${*D0p7v({GYYtW*;DU$kHp_RZFMuvyR6PW!^1NzNn_{G6 zXNT&+GKU$jX0LRLSZ7`7hTGrz9h#ZD9Bn#EvrV2$#DQdf|NRfTA^3=O`O5WGQ|C$8 z+d3g80o0q=pclX1uAG8dhc=9&j$m;yD;9zO-phF#8m2_Vn0(HC$Q0_H;!zcnT}r#3 zX0oN)eTm5XT3tubnH!$0KWZ#>R<{o}SBcNbvNcuG`Q?=!rdiE~+*GY;!x_N!yvD?0QW^EqMnQ_K5Rgj%g@C=E^+4;A zWwukr;SmKBGYavhc7Ut?`M2>zmYw}KY~rrYO~hCvNK!93X}q4VSL zUfosV1)TMH(|#lnEQ4hh5v`YV>tjc{SMm+0rI&cCJATa(=Ri8SlnNUG^XK6ki1fs9 zGfT|#K}$aH=cWn_Q@6L~l3=%;&PwY0eW9y87V-~WZL_hR@1_mWFxct+Dbbl(T0*%o z<4<#%`K%E{wd(@{Kg?pKkI>Dw!JBcJ@VR&xMumKy#~flD0?gPfC0x`9OM(P2!Pjt~ z80aG<@|u;pK>naNAaaZ@lNfFvHk8ZkoVzzjt;UQe&v(=Zj{c@HKkU&@D!urc9f0G* z_<=VUAFXQQz;umKxS$q#_!|c$7v-b<`dWI|v>F1=BTBq@%cj6zt%xrL>YkTck_yyb z*|S?MpE2y_*Jx5w$fZEpjY^ufgZ}urzZ&cgc2@xb(|P^eo_B1#uIt?nJZ?4Y;V$^T6Hy*=y6)gDaHd_}sj(HKOAMfA1c6G#o)%E_AA0rTt{V1kD zs7lDY?I^6|;Scxptth6iCY>1aWr@}t`!$x3b#gedw&#PxNfE(}Yxf$Qs7Eb8w#;D6*A{=v}Xt; zDvPi}S0{Q?oq_4y>k7~uKETwRWnvtT!bzgeB_A$c8g$Npi_1IY&{;)*1_$VyBYot` zTVhUzCkLIjkJ!5ucYD&{jziYho8@MPt5b={zY_-$>Zt5VFthDr8BV=QxWTyLIj5 z>Ua#Q+=r!5Rrm!KlGq>=PK>6GP4G&>FA!h+Zaa?$(I16UenQz?)%G>`(uO+M?8Zlj ziBpz&S8y1MiUCRq4jXiE;T{^Yx;IkBiQx7pAxo}w8kan5m2KA(Vi>m!qO&H|tnhy5 z(lI=e>L-t7!Lk>RoJb*~LwA^1xF5^lb;|BDY-N%sLaeTGQE9Fu$JmLb+$xqiwy@hc zJ6!ghHod47giNnH5fO#%u}Fg}IUNtoVF^i-&*3kv(lI2ESur|D@QO%K1{+!`G;3o& z2_2M{A4h@~Yb8*)1xn1bi>)0??c$g`>~dym{rZA?StR)5Tg5j?^0@n z05E9U=yp^lcB48-Bsh_luu*W&~3go{!RY6=0z(j?g2!Mv#U#8S!092ZD97z6+mzM(dkCH zy*owIO_%w!hzxN^g8fu(GT^Gsr>S+5XB|lzu14f66TC#0EqoNqc5JSYvo~SJrF3x~ z-cf*&F_P^%AE9Iu{Y0mu)*^1NhReVT>Eu7fpP4F^9+nefAyda9{Y*DwlFwDp+pLBA z0v6S+^}Dfm?3+;5IUt?NIIgv6e7#&}qJvm>}N;!3M` z-veQU2m9`mW$KJYwS!|U>1MWZPFG_2X9O5& zh|u`$$NN%RVIbO6!!QfN1_dwP-qx`1Zm$5SIjlwb1GGy=1yJNPv%zM{Kp3{-vw@4K z>DUi6SdU>OP_-v*ivE~biiVBRdq>pk-@X7H{_dPx>as$pVEPc;#jcIG1`Sf&Z7r0! zX{G?!a8v2YktU49?HB(ye7BP6xnG?K!9JyQ4T6R47w1DBa@Ffked7zv3g>!=Pb4x# z603F)&#tfBOP$!_T8d#4!IDF?*QhFj7BVgCrE-t`xbmfHFf5zb!P~fFIFqcr$34;M zo}mg5iXLK8!eVhtIE)s^L5rx{c=(zEVs=4sd1mHFryQa8b(N9NjvWEzx=sc>9=ixR zSO?$ro67su{41^5-t3>|!^HBc+w}x@t;EmD6%4;eg$JHW;E*{&C+f{6@h}aeN7AY#PSWQqTZc4BPL8QIBmKjE{Bikxp6~HuC(<0Z|{sRpBe897kEbb?XMEJ+44O)u@3g=;mN@4)<$Ws;u16xeEAfIKNksn>5;1hXyuJ zkEy!~+1^i~_|~#P+!B3UrT=ixD@tk{;AwyP>4rn(9o%cZ^N91Ej)nKSrdwuv#N0<= z?2cMMzBn$wP3vKi4E$8m9y`%4PzM^{q#|KdhhAtLu3c6kSCR)<$|$ zy1H@w1G^0x3cuf`eG^~-Te0yOJG4~W&^&5ENtjf<@DlR&jX>V4QdLUK(T&vDQ! zD*fOUzLo)l8qvjG%jHPjonZ7&ktjKZ`Wx(u=trO<{+X9}h=FFzl!gY2(_ne#E4K(C zcXJm-?B2!Tq}@b%&Qw}jb(G3Jf#q1akY?ynN8qE3!Qh0l^ICD)E9^ato!r*%*cw4@ zf6zb(2{AhDhc0gFP#t6-*SXi%9iBXoq|M8~BU-JLL*`+=FD%~e;2{QuUmV&Ij2F?m zr)`GGgBAFRWOn5^(>P~FL~u(Ff~?;Wy?I1#zd?d0Xdl6+@iIHrx}~Hfx_e9yrwU}8 zVy$g7V0U|yKlol7{1|qr3I&S^$ z2e=hr>)dW0FW>t8&Izvheme@t+7@3oc8#9Q&ih+>tqN>egM2jsXJc<#_X4?vIeTxe zsLfRy)h=vyF*2mZq*WjFY235oN+A+uJC!u6R67aO740VWV@maln(eYYFFs931x%4p5mU)d;OM@#Sz|*ts+O=L-rc}1wntCMl$010N>=FS4tSgQdR}Sq zqE8Zy9JrlTOIbzot%w3xN~1xGDR|DHTBq$7GIuD~xuP0V*hV%)p*YZrfQ1Lx)3OAX zhMR(pA$0_k0RFUr0pG5Kp|_lPpsS)BU(Jcoy92^1{lyOaF%p5U`>LqUU9?a#%o5Tc zogKDP>VRg`t?pdheI7kT_~v_UP}NZhv@u%9DSS{H;zOG^LG--797Oa9h4k{!&o{LaN%ZP<)pGaABuvJLv1zD%UVmYfR? zaqg%X=T}9ukyMXJ;>_mHRQMyjM=JbrUKcAN)-p0jOGsWm#r~ZmGdcdHn{;0-8DvIc z)I*)ba}Qmi&Pd)2lhgGu()*|1t%06RuF@&RnFS6amah;QjUWM(HE_Rh_?L-eXcU%4 z>!<6$b%-w`2f3|L9Esy|KHlvaZgZkfb?m$JGRm1DUf>E(I)y{D?A!be5bov~PS;FV zdKkEa!yDR-Mttq38`0`0uxneGexz%h{nU<5_le^eAIriX>Xq7t$MZOrH>Dk65_>_Q zB{l$$qZysN&9iNauCHw_@uUVl7R6n$NY~R*jCv|1-J)8W7y(Hne*i5M(!NpLRx5z0 zbMf#G0ROU?L2d8pAypv{((yPWS7>B!>nb3HoNMENO<4^7EgO~W>n!l;2eRGO%_It` zq3yH7x;d-sg8}~)AM-8@e;uMx?Z+s+v6G3b{wX!KXLFNGK{Uy+Qh0W!*vU`rVV#oF z;B|XhpMx4Q$e*$xsSh9aQmgZbDFXHb{S}Ml1)y)A?JSCv% zgBr}g9ovQrb5n$>k>hIgJeKz?AgZ4Xq!YcE(RVo^89+1(DY1qP?NR*xJj!spZn-jO z#zAkJOp5u2g!5SU0%H|9IY{UfNdW792B-NymM^v!ZtOl(wFnF;y-!3 z(h!wvzDkacz}!`LwDHJ?e<`ZO#9Pohe**|mnp@N#z8D^AIW_xOzNWjiXN_|oRVAcQ zr>8-$ce~PXOq`-VFLOV$C@@~4Y`rYgrwJRp=RGdD3x88s%obQ2r}TgbRNplUEc)$y zjc%__8ru#lFP7^aZ()8U*xMrG``k3h>s>q)B$(Qir|nOxJhP}1MNtb*D*I$T-8y(p z~kLjkT0gSl1r0= zJ==(bdBQ1r&o%%C@=IWCSKFgzEr{igy;X8oTfVdp`_#ZQg!9u3vdcQB25qogx+V6r zz$vdv1zJFEDFpN%Du7wvE6~sMD3#>3V<*IK_pXcGQ3?T~9f|k~t7v*1@ zihuc)!5_nXMy=l(W;LjyMsYZYdf~0PWN2|K!K;r9!1z%(O1BH2&vYzCpe+`Q>+4~$ z)?d&|wO4}F?e}d8OK*V@=_Af!on_PMNafqbu$fiN`on>o<8+wHzav` zDpi{0tj>iCq|vAFo(8PB_(F*{oK^Za_bPi#iDw!hOck5M$+(|n@@6)w&wSuqahiMbnv=D{kQFQHe3XkOHMaXVDvlwHAqykB~U=<3!tzj4Giv9az=c;L*v5CZ)+#$R8pW5-f?V3L}IplkrXw?rcF|;0E;Ns^W zpMPG2<3YbGwxtGBG1?^qxklgI2K7ZUHaZlR$d26u^d7RT6$E4D`)???6?h4i%M7SG z@5@pxf6Pp^qirzEP48cU^OjEqN#ItrO3#Us=tBDMdbsi3bnBlw3)65D~L~5!_ z3vdzkBzf`AQ>0}fIx8s7I`bAW@Y4^une!91e*zafZdEi(M5qeTFCPlCvhISCYf#?o zsyxXAe>ce(uDH%fM0j`%!uqem~$L@P?=Umd}^gdb@B)O=J< z?9A6CG+3GT`k}ghsR~^gp6(#0FHdY3J{3toUFZR>*%1nirP_Z)ifUnL*^w^J=^az~ z^w`#f3_<-Wx#U>U;i6T2&y!7)j}wv^p;tUf?FT)wsk=5JfbD+DjE71Sk?*><8eBCv z%E@7#;dAJPe}ynuiBHrUJ&WSyxB_m^i!-9}CaKs2lBkRo!UK8jO5J%=r7Lr(F8YNS zQ`QManyZfgoYa883zGpMOm_tdrD7C&odsR5TG_>7uG;QyrnQovCXzceA1fHnmIRmk zZ2>^^!?WAzNj+wNJUsv|(-iXJ4gdgI1A@8oc`Vz1IW_7Z?=k26xG?&qr+$S=o8DbM zIv^x3diSL21`9W|>Tpa;(j-T^)U`syQDFs<1?kL(JCg4a*|sgtI9JIAExFNzeWC77 z1{Z5{Jb1Ca_?RmmigF4I@rqGJrebZUC=aR%2_X0(V*B7qwkyIX$tUGzN#e+E5_jJ} zW%R9$L(cuS+U0;$#Ora|BPJ)Qq2x4l3O^s~jqtclhwnMhFH>%O$ykQO{dM=y{V$ z6#dV9si4#nolyko^V6DRBu*pMZSZO2GN2p*$F$CT&JA24HJ*-aZ^pvE>wN7KH z3EPx{L~?lCUS;<$UrWzu8^{^&Ryq^#If_NG4XxB55@5Wv@_upRT2mg@K7CV&cvn-! zH8U848xo(T#D|Fwo(@K^qKGlubt9ufwC@**3}2y`*KJ zjyys4Tw^=UO_{8grj&uW1X4^T5W8Jo-xN7{SoX^w^P*a~>W>X;YxUkXKQGJkUN{6u zH53s>9JkO?r%#LxDCDbt>}@i8{Q2?ive>cTJTLv|KFAf=zFv3tBgLGu;k5g! zBo0WbRK~|8Ac>GhZp&-1WTCe8VvU zug4yq+?B+sfcwqQv`N2|Ah!!>{$&)Ql7m!gse``Ehw@5a^o=domW!!dn7qN*S(`~La8=f~75~}B5QuyA4{0e{Sv9qo$!SRN z3vasKFCJ*%IG-H?&_nJIWJV+8j6}74ZI_Awpz$a0fftgpVYfw@PJa8HlAv7V=E1`z zAYJ!eJc04sA1`+Y5kJvC&vireDH&mnQowa9YS8S0AFnIIoIABRS*6arkmIF?^*+a5 zS?j2&-l+FcQ!5JsZ!0o*jqh=`j4&j`3_;44@DqF{Vw8-I;V8wQsOfl&|B4anf`j`r z`d9uibY%+Qx-Jhwq$Xv(tA+!F!nt#Ij=9G$hTqQZ&&r3AwAL^eUdFEk1Z{1oLTpj* z!KCzG*bU#~l2I&%h$|fkM|TlJ1uh2laqA$$e~#8$ZpRc&I3;|nw11dvUC?lZ2h(7! z_M@?+_`LPna*XIhAY*%l@)$V1xz_ifO)56&y=|e$`OKL1{%F%p zQHX(wrCUK4M%@>gMX@hi3Zs_khN zHxIzjX9Y5z7yXrIk8Y-r0ssd>AQW!sun|^P!)QNe6KQh}d+;3W#-$WWXj*QPr1Der zKy{75w&~IC_nr(SIVJg$3+Afw2EKdK>QRIIotq0=@DjvZ>~Q&|4uagD8a`0WW( z9%mtBB6)LtcS^;g`Zvo$!B^bAJa?@!wrT09w>>^RY^{_47PP)AX1hIC6o*-kHs6Br z85S`iLnU9q4!%b-kL0w#DmdT$Vf>CSvw!V9QRmwv^GrIxlLD1i(}eOQTP(s8LZjZ7 z-Gijr6c(BgT5l|^AqGhaIwC`yu=z2p_2(x6?rH3Uiex|nrewyK_A%UN-<_^OTi+_0 z1F3Vrfal~{K`7I(JP06_y^w6V@z&SCV{xvjsFk6gOA(Py^TwIxNE2h4{BwC#+Vr;P z{*<+Pfk~t>Fwjxu9 zzJ@>TaC(@sc_XP7apm%rCx0hw1Fc9rnTrU1d~lGF=Wk~N6*7g$@C+^JCaNo)7n2xXSp zcL~;Q&p1@eFis4Ee0%XoQ%-2e(CfO#52`>@YsWbXdZ9~3=R}fpA$rZTzRd@Bpi+BC zSUug^_?WaEbP=}qq$26m1a_*BWEIt-wTi14J-o&NO~t}m9S>`Bfv3Rv_$hw^D=Jueq5zh zcB3_ukD)uAJPnueZ^RyViKq8nKO<0HCu(deibykk|F}ueQa=SLcIuc&J8n?QjSN;X zTPRdL4=lON`vj{!Vog&hHUBcBN0uT|hjQ0!ufvP-Acm-*poF! z7{`+)h$`H)X~IYx*%>!&C3(5fJcRtNeEU^FipRx}cO^#*UD;y^xgm0RutdgJSyYX( zy??%+7SMJhVdZullehwf-ncWH09BASz7)lPC~QJvI;>jUXLh|u1rmQ6^=lA?AW3pr zZMeLYkRB(`6RzAfg!7jx0v{WL78Uz=j1DX8Q46L!d4`6qy%x5~Ar?KxpOHI;vp~O< zg~UE_9BQp$dlG_7!j%#^o`W!>E(ni~%$kxLYoTnTKT&8cS@Bb*r1r!gc^sKIc26^c zift=KDx1w=`$Jg|#2xH1(4K$L%Rb&aV7j$<30Fc#GR&$Wl$q0$Jm3#!qs{hb-XAu$ zd4-$%a>7`~@?PqG{5Ua5=^#Oyw$AM94+&?DKC2mX(YrBq~4798V&SkeIKP;Ukon-OG-I2h_oYA4JK-w?~&qoi4 zPuPJgy*h09o>zy?Ypj`fGl7W%RWGdR%&yQU8ry~*7Ar?2%}9Nug(1#zA(~A{6bZWI z!({A;!t1}5n@c!a-UT8Z3gf%kOH8aFzo}lbdRDmXDdvdp5CS?hBjx}|1E06A{WBYY zD*Zg5Sz9QL6f?-@Jqr;4Pdsr6L?~OnC&74K;PC5hB)o;KLiu&Ow@IFUvhbOe88kRH zl#^-V7n6dpGmu%84reaIX(-uDdhSK-13Qj_M})~7Kp}A(vZfa*6L3_@jqV-Pl zqh$2E@uf$PYzWUSbEqfZ6A1>su{l+0_V>n~XfkW6LHW7_ z+kHO;KP^8ZhW5W9JNiqdpJXS0DfCHp`IlsV|0K(X|6h@P9*XeK-^71!*nh4z5ipzK ze-z63R7fV2&ia>RzW*Xq`rnb+{iV`RGKaqu`XqDxOS1p4S<+vSZGBGQQ)%P>sPrE@ zGzz~ZzYp92pdc%s dSL)AF(Eq>XvrjN5RvBTRx2yjf7Xae!{{drs;BWu{ literal 0 HcmV?d00001 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");