tempest-react-sdk 0.81.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +5 -6
  2. package/bin/create-tempest-app.e2e.test.mjs +31 -0
  3. package/bin/create-tempest-app.mjs +11 -1
  4. package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
  5. package/bin/lib/doctor/vitest.mjs +120 -0
  6. package/bin/lib/doctor/vitest.test.mjs +139 -0
  7. package/bin/lib/fs/contain.mjs +7 -0
  8. package/bin/lib/openapi/escape.mjs +68 -4
  9. package/bin/lib/openapi/generate.hostile.test.mjs +137 -0
  10. package/bin/lib/openapi/generate.mjs +78 -13
  11. package/bin/lib/openapi/pattern-risk.mjs +134 -0
  12. package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
  13. package/bin/tempest-gen.test.mjs +100 -0
  14. package/bin/tempest.mjs +24 -8
  15. package/dist/access/use-can.cjs +1 -1
  16. package/dist/access/use-can.cjs.map +1 -1
  17. package/dist/access/use-can.js +25 -28
  18. package/dist/access/use-can.js.map +1 -1
  19. package/dist/auth/create-tempest-auth.cjs +1 -1
  20. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  21. package/dist/auth/create-tempest-auth.js +192 -149
  22. package/dist/auth/create-tempest-auth.js.map +1 -1
  23. package/dist/auth/session-identity.cjs +2 -0
  24. package/dist/auth/session-identity.cjs.map +1 -0
  25. package/dist/auth/session-identity.js +12 -0
  26. package/dist/auth/session-identity.js.map +1 -0
  27. package/dist/br/boleto.cjs.map +1 -1
  28. package/dist/br/boleto.js.map +1 -1
  29. package/dist/components/AuthForm/AuthLinks.cjs +1 -1
  30. package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
  31. package/dist/components/AuthForm/AuthLinks.js +18 -15
  32. package/dist/components/AuthForm/AuthLinks.js.map +1 -1
  33. package/dist/components/AuthPage/AuthPage.cjs +1 -1
  34. package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
  35. package/dist/components/AuthPage/AuthPage.js +58 -52
  36. package/dist/components/AuthPage/AuthPage.js.map +1 -1
  37. package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
  38. package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
  39. package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
  40. package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  41. package/dist/components/Button/Button.cjs +1 -1
  42. package/dist/components/Button/Button.cjs.map +1 -1
  43. package/dist/components/Button/Button.js +41 -40
  44. package/dist/components/Button/Button.js.map +1 -1
  45. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  46. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  47. package/dist/components/Chat/ChatAttachments.js +38 -38
  48. package/dist/components/Form/Form.cjs +1 -1
  49. package/dist/components/Form/Form.cjs.map +1 -1
  50. package/dist/components/Form/Form.js +30 -32
  51. package/dist/components/Form/Form.js.map +1 -1
  52. package/dist/components/Layout/Layout.cjs +1 -1
  53. package/dist/components/Layout/Layout.cjs.map +1 -1
  54. package/dist/components/Layout/Layout.js +25 -24
  55. package/dist/components/Layout/Layout.js.map +1 -1
  56. package/dist/components/Layout/gap.cjs +2 -0
  57. package/dist/components/Layout/gap.cjs.map +1 -0
  58. package/dist/components/Layout/gap.js +15 -0
  59. package/dist/components/Layout/gap.js.map +1 -0
  60. package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
  61. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  62. package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
  63. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  64. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  65. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  66. package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
  67. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
  68. package/dist/components/Sidebar/SidebarEntry.js +30 -29
  69. package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
  70. package/dist/data/create-data-provider.cjs +1 -1
  71. package/dist/data/create-data-provider.cjs.map +1 -1
  72. package/dist/data/create-data-provider.js +1 -2
  73. package/dist/data/create-data-provider.js.map +1 -1
  74. package/dist/forms/validate-form.cjs +1 -1
  75. package/dist/forms/validate-form.cjs.map +1 -1
  76. package/dist/forms/validate-form.js +6 -1
  77. package/dist/forms/validate-form.js.map +1 -1
  78. package/dist/forms/zod-resolver.cjs +1 -1
  79. package/dist/forms/zod-resolver.cjs.map +1 -1
  80. package/dist/forms/zod-resolver.js +8 -3
  81. package/dist/forms/zod-resolver.js.map +1 -1
  82. package/dist/http/api-client.cjs +1 -1
  83. package/dist/http/api-client.cjs.map +1 -1
  84. package/dist/http/api-client.js +90 -72
  85. package/dist/http/api-client.js.map +1 -1
  86. package/dist/http/credential-scope.cjs +1 -1
  87. package/dist/http/credential-scope.cjs.map +1 -1
  88. package/dist/http/credential-scope.js +33 -9
  89. package/dist/http/credential-scope.js.map +1 -1
  90. package/dist/http/presigned-post.cjs.map +1 -1
  91. package/dist/http/presigned-post.js.map +1 -1
  92. package/dist/http/resumable-upload.cjs +1 -1
  93. package/dist/http/resumable-upload.cjs.map +1 -1
  94. package/dist/http/resumable-upload.js +90 -84
  95. package/dist/http/resumable-upload.js.map +1 -1
  96. package/dist/http/upload-with-progress.cjs +1 -1
  97. package/dist/http/upload-with-progress.cjs.map +1 -1
  98. package/dist/http/upload-with-progress.js +8 -8
  99. package/dist/http/upload-with-progress.js.map +1 -1
  100. package/dist/oauth/GoogleSignIn.cjs +1 -1
  101. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  102. package/dist/oauth/GoogleSignIn.js +5 -4
  103. package/dist/oauth/GoogleSignIn.js.map +1 -1
  104. package/dist/offline/create-offline-sync.cjs +1 -1
  105. package/dist/offline/create-offline-sync.cjs.map +1 -1
  106. package/dist/offline/create-offline-sync.js +87 -69
  107. package/dist/offline/create-offline-sync.js.map +1 -1
  108. package/dist/query/offline-persistence.cjs +1 -1
  109. package/dist/query/offline-persistence.cjs.map +1 -1
  110. package/dist/query/offline-persistence.js +54 -44
  111. package/dist/query/offline-persistence.js.map +1 -1
  112. package/dist/sse/create-event-stream.cjs +1 -1
  113. package/dist/sse/create-event-stream.cjs.map +1 -1
  114. package/dist/sse/create-event-stream.js +48 -41
  115. package/dist/sse/create-event-stream.js.map +1 -1
  116. package/dist/sse/ticket-event-stream.cjs +1 -1
  117. package/dist/sse/ticket-event-stream.cjs.map +1 -1
  118. package/dist/sse/ticket-event-stream.js +48 -19
  119. package/dist/sse/ticket-event-stream.js.map +1 -1
  120. package/dist/sse/use-event-stream.cjs +1 -1
  121. package/dist/sse/use-event-stream.cjs.map +1 -1
  122. package/dist/sse/use-event-stream.js +19 -14
  123. package/dist/sse/use-event-stream.js.map +1 -1
  124. package/dist/sw/background-sync.cjs +1 -1
  125. package/dist/sw/background-sync.cjs.map +1 -1
  126. package/dist/sw/background-sync.js +10 -8
  127. package/dist/sw/background-sync.js.map +1 -1
  128. package/dist/sw/cache.cjs +1 -1
  129. package/dist/sw/cache.cjs.map +1 -1
  130. package/dist/sw/cache.js +36 -30
  131. package/dist/sw/cache.js.map +1 -1
  132. package/dist/sw/create-push-handler.cjs +1 -1
  133. package/dist/sw/create-push-handler.cjs.map +1 -1
  134. package/dist/sw/create-push-handler.js +20 -10
  135. package/dist/sw/create-push-handler.js.map +1 -1
  136. package/dist/sw.d.ts +44 -13
  137. package/dist/tabular/compact.cjs +1 -1
  138. package/dist/tabular/compact.cjs.map +1 -1
  139. package/dist/tabular/compact.js +35 -16
  140. package/dist/tabular/compact.js.map +1 -1
  141. package/dist/tabular/manifest.cjs +1 -1
  142. package/dist/tabular/manifest.cjs.map +1 -1
  143. package/dist/tabular/manifest.js +33 -15
  144. package/dist/tabular/manifest.js.map +1 -1
  145. package/dist/tabular.d.ts +11 -1
  146. package/dist/tempest-react-sdk.cjs +1 -1
  147. package/dist/tempest-react-sdk.d.ts +457 -68
  148. package/dist/tempest-react-sdk.js +224 -224
  149. package/dist/theme/initial-theme.cjs +4 -4
  150. package/dist/theme/initial-theme.cjs.map +1 -1
  151. package/dist/theme/initial-theme.js +8 -5
  152. package/dist/theme/initial-theme.js.map +1 -1
  153. package/dist/utils/compressed-storage.cjs +1 -1
  154. package/dist/utils/compressed-storage.cjs.map +1 -1
  155. package/dist/utils/compressed-storage.js +13 -7
  156. package/dist/utils/compressed-storage.js.map +1 -1
  157. package/dist/utils/objects.cjs +1 -1
  158. package/dist/utils/objects.cjs.map +1 -1
  159. package/dist/utils/objects.js +13 -7
  160. package/dist/utils/objects.js.map +1 -1
  161. package/dist/utils/safe-url.cjs +1 -1
  162. package/dist/utils/safe-url.cjs.map +1 -1
  163. package/dist/utils/safe-url.js +4 -1
  164. package/dist/utils/safe-url.js.map +1 -1
  165. package/dist/webrtc/peer-mesh.cjs +1 -1
  166. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  167. package/dist/webrtc/peer-mesh.js +7 -1
  168. package/dist/webrtc/peer-mesh.js.map +1 -1
  169. package/dist/ws/create-web-socket.cjs +1 -1
  170. package/dist/ws/create-web-socket.cjs.map +1 -1
  171. package/dist/ws/create-web-socket.js +77 -61
  172. package/dist/ws/create-web-socket.js.map +1 -1
  173. package/dist/ws/use-web-socket.cjs +1 -1
  174. package/dist/ws/use-web-socket.cjs.map +1 -1
  175. package/dist/ws/use-web-socket.js +4 -3
  176. package/dist/ws/use-web-socket.js.map +1 -1
  177. package/package.json +1 -1
  178. package/template-pwa/src/pwa.tsx +42 -0
  179. package/template-pwa/src/sw.ts +5 -4
package/dist/sw.d.ts CHANGED
@@ -336,7 +336,9 @@ export declare interface InstallBackgroundSyncOptions {
336
336
  *
337
337
  * A click on an action button opens that action's URL (see
338
338
  * {@link PushNotificationAction.url}); a click on the body, or on an action
339
- * without one, opens the top-level `url`.
339
+ * without one, opens the top-level `url`. Only a same-origin `http(s)` URL is
340
+ * opened by default; see {@link InstallNotificationClickHandlerOptions.resolveUrl}
341
+ * for opting into another origin.
340
342
  */
341
343
  export declare function installNotificationClickHandler(options?: InstallNotificationClickHandlerOptions): void;
342
344
 
@@ -345,6 +347,12 @@ export declare interface InstallNotificationClickHandlerOptions {
345
347
  * Resolve the destination URL from the notification data and the clicked
346
348
  * action id (`""` or `undefined` for a click on the body). Default: the
347
349
  * action's entry in `data.actionUrls`, else `data.url`, else `"/"`.
350
+ *
351
+ * The default only ever opens a page of the worker's **own origin**: a URL on
352
+ * another origin, or with a scheme other than `http:`/`https:`, becomes `"/"`.
353
+ * A resolver of your own is how an app opts into another origin — what it
354
+ * returns may point anywhere `http:`/`https:`, and any other scheme still
355
+ * becomes `"/"`.
348
356
  */
349
357
  resolveUrl?: (data: unknown, action: string | undefined) => string;
350
358
  }
@@ -389,8 +397,9 @@ export declare interface InstallPrecacheOptions {
389
397
  * Every `NotificationOptions` member the payload carries is forwarded —
390
398
  * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,
391
399
  * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and
392
- * `tag`. `data` receives the top-level `url` (default `"/"`), the payload's
393
- * own `data`, and `actionUrls` mapping each action id to its `url`.
400
+ * `tag`. `data` receives the payload's own `data`, then the top-level `url`
401
+ * (default `"/"`) and `actionUrls` mapping each action id to its `url` — in that
402
+ * order, so `payload.data` cannot override where a click goes.
394
403
  */
395
404
  export declare function installPushHandler(options?: InstallPushHandlerOptions): void;
396
405
 
@@ -414,9 +423,10 @@ export declare interface InstallPushHandlerOptions {
414
423
  * `respondWith`), so a later {@link installPrecache} can handle them.
415
424
  *
416
425
  * A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the
417
- * route's `credentialHeaders`) is also left untouched unless its route sets
426
+ * route's `credentialHeaders`), or a CORS `fetch` sent with
427
+ * `credentials: "include"`, is also left untouched unless its route sets
418
428
  * `cacheAuthenticated`, and a response with `Cache-Control: no-store`
419
- * (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`
429
+ * (or `private`, without that opt-in) or `Vary: *` is served but never stored. `maxAgeSeconds`
420
430
  * applies to every read, including the offline fallback: an expired entry is a
421
431
  * miss, never a last resort. Under `rangeRequests`, the full resource is cached
422
432
  * once (the `Range` header stripped) and each range is sliced from it.
@@ -457,7 +467,13 @@ export declare function installSkipWaitingListener(): void;
457
467
  *
458
468
  * Pass `getPrincipal` to bind each entry to whoever queued it: the worker asks
459
469
  * the issuing tab right after queueing, asks every tab again before replaying,
460
- * and drops the entry when no signed-in tab is its owner.
470
+ * and drops the entry when no signed-in tab is its owner. Without it every
471
+ * entry is `"unbound"` and replays with whichever session answers — the next
472
+ * account's bearer included, unless `clearSessionData()` ran on logout — so a
473
+ * development build warns once per call (#531). Treating an unbound
474
+ * credentialed entry as `"unknown"` instead would change nothing: an
475
+ * `"unknown"` entry replays when no answering tab reports a principal, which is
476
+ * exactly the page that omits `getPrincipal`.
461
477
  *
462
478
  * @param getHeaders - Resolves the headers for one queued request at replay.
463
479
  * @param options - `getPrincipal` binds entries to their owner.
@@ -548,7 +564,14 @@ export declare interface PushPayload {
548
564
  dir?: NotificationDirection;
549
565
  /** BCP 47 language tag of the title and body. */
550
566
  lang?: string;
551
- /** Arbitrary extra data forwarded to `event.notification.data`. */
567
+ /**
568
+ * Arbitrary extra data forwarded to `event.notification.data`.
569
+ *
570
+ * Its `url` and `actionUrls` keys are overwritten by the top-level `url` and
571
+ * the actions' `url`s: those two keys decide where a click goes, and a
572
+ * free-form bag — often filled from the record that triggered the push — must
573
+ * not be able to redirect it.
574
+ */
552
575
  data?: Record<string, unknown>;
553
576
  }
554
577
 
@@ -679,8 +702,9 @@ export declare interface RuntimeRoute {
679
702
  */
680
703
  rangeRequests?: boolean;
681
704
  /**
682
- * Opt in to caching requests that carry a credential header (see
683
- * `credentialHeaders`), and responses marked `Cache-Control: private`.
705
+ * Opt in to caching credentialed requests — those that carry a credential
706
+ * header (see `credentialHeaders`) or are a CORS-mode `fetch` sent with
707
+ * `credentials: "include"` — and responses marked `Cache-Control: private`.
684
708
  * Default `false`.
685
709
  *
686
710
  * Cache Storage is keyed by URL and outlives the session: without this
@@ -697,11 +721,18 @@ export declare interface RuntimeRoute {
697
721
  * `clearSessionData()` on logout, which removes the entries themselves.
698
722
  * `Cache-Control: no-store` is never cached, opt-in or not.
699
723
  *
700
- * A cookie session is invisible here: the worker cannot see `Cookie`, and
701
- * gating on `request.credentials` would switch off caching for every
702
- * same-origin image and font. A cookie-authenticated API stays private only
724
+ * A cookie session is only partly visible here: the worker cannot see
725
+ * `Cookie`. What it can see is a `fetch` that asked for cookies explicitly —
726
+ * `mode: "cors"` with `credentials: "include"`, which is what `ApiClient`
727
+ * sends under `withCredentials` — and since 0.82.0 that request is treated
728
+ * as credentialed too (#531). `<img>`, `<audio>` and `<video>` also send
729
+ * `credentials: "include"`, but in `no-cors` mode, so they keep caching. A
730
+ * plain same-origin `fetch` (`credentials: "same-origin"`, the default)
731
+ * carries the cookie without saying so; such an API stays private only
703
732
  * through `Cache-Control: private`/`no-store` on its responses and
704
- * `clearSessionData()` on logout.
733
+ * `clearSessionData()` on logout. Opting a cookie request in caches it by
734
+ * URL — there is no credential header to vary on — so `clearSessionData()`
735
+ * is then its only isolation.
705
736
  */
706
737
  cacheAuthenticated?: boolean;
707
738
  /**
@@ -1,2 +1,2 @@
1
- const e=require("./exceptions.cjs");var t=`TMC1`,n=class n{header;arrays;info;constructor(e,t,n){this.header=e,this.arrays=t,this.info=n}static async create(i,a){let o=await r(i,a),s=new Uint8Array(o);if(String.fromCharCode(...s.subarray(0,4))!==t)throw new e.CompactFormatError(`This is not a compact model file: it does not start with "${t}". A .onnx file goes through TabularPredictor instead.`);let c=new DataView(o).getUint32(4,!0),l=JSON.parse(new TextDecoder().decode(s.subarray(8,8+c)));if(l.schema_version>1)throw new e.CompactFormatError(`This file uses compact layout ${l.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving it.`);let u={},d=8+c;for(let e of l.sections)u[e.name]=e.dtype===`float32`?new Float32Array(o,d,e.length):new Int32Array(o,d,e.length),d+=e.length*4;return new n(l,u,{kind:l.kind,classes:l.classes,numFeatures:l.n_features,featureNames:l.feature_names,numTrees:l.n_trees??0,isClassifier:l.task===`classification`,estimator:l.estimator})}async predict(e){let t=i(e,this.header.n_features),n=performance.now(),r=[];for(let n of e){let e=this.preprocess(n,t);r.push(this.header.kind===`linear`?this.linearScores(e):this.treeScores(e))}let{labels:a,probabilities:o}=this.finish(r);return{labels:a,probabilities:o,numRows:e.length,ms:performance.now()-n}}async dispose(){}preprocess(e,t){let n=this.header.preprocess;if(n===null)return e;let r=Array(t);for(let i=0;i<t;i+=1)r[i]=(e[i]-n.offset[i])/n.scale[i];return r}linearScores(e){let t=this.arrays.coef,n=this.arrays.intercept,r=this.header.n_features,i=this.header.n_outputs,a=Array(i);for(let o=0;o<i;o+=1){let i=n[o],s=o*r;for(let n=0;n<r;n+=1)i+=t[s+n]*e[n];a[o]=i}return a}treeScores(e){let t=this.arrays.node_feature,n=this.arrays.node_threshold,r=this.arrays.node_left,i=this.arrays.node_right,a=this.arrays.leaf_value,o=this.arrays.tree_offset,s=this.header.n_outputs,c=o.length-1,l=Array(s).fill(0);for(let u=0;u<c;u+=1){let c=o[u],d=t[c];for(;d>=0;)c=Math.fround(e[d])<=n[c]?r[c]:i[c],d=t[c];let f=(-1-d)*s;for(let e=0;e<s;e+=1)l[e]+=a[f+e]}for(let e=0;e<s;e+=1)l[e]=l[e]/c;return l}finish(e){let t=this.header.link,n=this.header.classes;if(t===`identity`)return{labels:e.map(e=>e[0]),probabilities:[]};let r=e.map(e=>{if(t===`sigmoid`){let t=1/(1+Math.exp(-e[0]));return[1-t,t]}if(t===`softmax`){let t=Math.max(...e),n=e.map(e=>Math.exp(e-t)),r=n.reduce((e,t)=>e+t,0);return n.map(e=>e/r)}let n=e.reduce((e,t)=>e+t,0);return n===0?[...e]:e.map(e=>e/n)}),i=this.header.class_type!==`str`;return{labels:r.map(e=>{let t=0;for(let n=1;n<e.length;n+=1)e[n]>e[t]&&(t=n);let r=n[t];return r===void 0?t:i?Number(r):r}),probabilities:r}}};async function r(t,n){if(typeof t==`string`){let r;try{r=await fetch(t,n)}catch(n){throw new e.ModelFetchError(`Could not download the model: ${t}`,{cause:n})}if(!r.ok)throw new e.ModelFetchError(`Could not download the model: ${r.status} ${r.statusText}`);return await r.arrayBuffer()}return t instanceof Uint8Array?t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength):t}function i(t,n){if(!Array.isArray(t)||t.length===0)throw new e.FeatureShapeError(`predict() needs at least one row, shaped [[f1, f2, ...]].`);let r=t[0]?.length??0,i=t.findIndex(e=>e.length!==r);if(i!==-1)throw new e.FeatureShapeError(`All rows must have the same width; row ${i} has ${t[i]?.length} values, expected ${r}.`);if(r!==n)throw new e.FeatureShapeError(`The model expects ${n} features per row, got ${r}.`);return r}exports.CompactPredictor=n;
1
+ const e=require("./exceptions.cjs");var t=`TMC1`,n=class n{header;arrays;info;constructor(e,t,n){this.header=e,this.arrays=t,this.info=n}static async create(a,o){let s=await r(a,o),c=new Uint8Array(s);if(String.fromCharCode(...c.subarray(0,4))!==t)throw new e.CompactFormatError(`This is not a compact model file: it does not start with "${t}". A .onnx file goes through TabularPredictor instead.`);let l=new DataView(s).getUint32(4,!0),u=JSON.parse(new TextDecoder().decode(c.subarray(8,8+l)));if(u.schema_version>1)throw new e.CompactFormatError(`This file uses compact layout ${u.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving it.`);let d={},f=8+l;for(let e of u.sections)d[e.name]=e.dtype===`float32`?new Float32Array(s,f,e.length):new Int32Array(s,f,e.length),f+=e.length*4;return u.kind===`tree_ensemble`&&i(u,d),new n(u,d,{kind:u.kind,classes:u.classes,numFeatures:u.n_features,featureNames:u.feature_names,numTrees:u.n_trees??0,isClassifier:u.task===`classification`,estimator:u.estimator})}async predict(e){let t=a(e,this.header.n_features),n=performance.now(),r=[];for(let n of e){let e=this.preprocess(n,t);r.push(this.header.kind===`linear`?this.linearScores(e):this.treeScores(e))}let{labels:i,probabilities:o}=this.finish(r);return{labels:i,probabilities:o,numRows:e.length,ms:performance.now()-n}}async dispose(){}preprocess(e,t){let n=this.header.preprocess;if(n===null)return e;let r=Array(t);for(let i=0;i<t;i+=1)r[i]=(e[i]-n.offset[i])/n.scale[i];return r}linearScores(e){let t=this.arrays.coef,n=this.arrays.intercept,r=this.header.n_features,i=this.header.n_outputs,a=Array(i);for(let o=0;o<i;o+=1){let i=n[o],s=o*r;for(let n=0;n<r;n+=1)i+=t[s+n]*e[n];a[o]=i}return a}treeScores(e){let t=this.arrays.node_feature,n=this.arrays.node_threshold,r=this.arrays.node_left,i=this.arrays.node_right,a=this.arrays.leaf_value,o=this.arrays.tree_offset,s=this.header.n_outputs,c=o.length-1,l=Array(s).fill(0);for(let u=0;u<c;u+=1){let c=o[u],d=t[c];for(;d>=0;)c=Math.fround(e[d])<=n[c]?r[c]:i[c],d=t[c];let f=(-1-d)*s;for(let e=0;e<s;e+=1)l[e]+=a[f+e]}for(let e=0;e<s;e+=1)l[e]=l[e]/c;return l}finish(e){let t=this.header.link,n=this.header.classes;if(t===`identity`)return{labels:e.map(e=>e[0]),probabilities:[]};let r=e.map(e=>{if(t===`sigmoid`){let t=1/(1+Math.exp(-e[0]));return[1-t,t]}if(t===`softmax`){let t=Math.max(...e),n=e.map(e=>Math.exp(e-t)),r=n.reduce((e,t)=>e+t,0);return n.map(e=>e/r)}let n=e.reduce((e,t)=>e+t,0);return n===0?[...e]:e.map(e=>e/n)}),i=this.header.class_type!==`str`;return{labels:r.map(e=>{let t=0;for(let n=1;n<e.length;n+=1)e[n]>e[t]&&(t=n);let r=n[t];return r===void 0?t:i?Number(r):r}),probabilities:r}}};async function r(t,n){if(typeof t==`string`){let r;try{r=await fetch(t,n)}catch(n){throw new e.ModelFetchError(`Could not download the model: ${t}`,{cause:n})}if(!r.ok)throw new e.ModelFetchError(`Could not download the model: ${r.status} ${r.statusText}`);return await r.arrayBuffer()}return t instanceof Uint8Array?t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength):t}function i(t,n){let r=n.node_feature,i=n.node_left,a=n.node_right,o=n.leaf_value,s=n.tree_offset;if(!r||!i||!a||!o||!s||s.length<2)throw new e.CompactFormatError(`This tree model is missing its node or offset sections.`);let c=r.length,l=t.n_outputs,u=t=>new e.CompactFormatError(`This tree model is malformed: ${t}. Re-export it.`);if(i.length!==c||a.length!==c||s[0]!==0)throw u(`the node sections disagree on the node count`);if(s[s.length-1]!==c)throw u(`the last tree does not end at the last node`);for(let e=0;e<s.length-1;e+=1){let n=s[e],c=s[e+1];if(c<=n)throw u(`tree ${e} has no nodes`);for(let e=n;e<c;e+=1){let n=r[e];if(n>=0){let r=i[e],o=a[e];if(n>=t.n_features)throw u(`node ${e} splits on feature ${n}`);if(r<=e||o<=e||r>=c||o>=c)throw u(`node ${e} points outside its tree or back up it`)}else if((-1-n)*l+l>o.length)throw u(`leaf ${e} reads past the leaf values`)}}}function a(t,n){if(!Array.isArray(t)||t.length===0)throw new e.FeatureShapeError(`predict() needs at least one row, shaped [[f1, f2, ...]].`);let r=t[0]?.length??0,i=t.findIndex(e=>e.length!==r);if(i!==-1)throw new e.FeatureShapeError(`All rows must have the same width; row ${i} has ${t[i]?.length} values, expected ${r}.`);if(r!==n)throw new e.FeatureShapeError(`The model expects ${n} features per row, got ${r}.`);return r}exports.CompactPredictor=n;
2
2
  //# sourceMappingURL=compact.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"compact.cjs","names":["CompactFormatError","ModelFetchError","FeatureShapeError","require_exceptions","MAGIC","CompactPredictor","header","arrays","info","buffer","toBuffer","source","requestInit","bytes","headerLength","cursor","section","width","validateRows","rows","started","scores","row","prepared","preprocess","scaled","index","coef","intercept","features","outputs","output","total","base","feature","threshold","left","right","leaf","offsets","trees","totals","tree","node","column","slot","link","classes","probabilities","positive","highest","exponentiated","value","sum","numeric","best","label","response","error","ragged","expected"],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"compact.cjs","names":["CompactFormatError","ModelFetchError","FeatureShapeError","require_exceptions","MAGIC","CompactPredictor","header","arrays","info","buffer","toBuffer","source","requestInit","bytes","headerLength","cursor","section","assertTreesTerminate","width","validateRows","rows","started","scores","row","prepared","preprocess","scaled","index","coef","intercept","features","outputs","output","total","base","feature","threshold","left","right","leaf","offsets","trees","totals","tree","node","column","slot","link","classes","probabilities","positive","highest","exponentiated","value","sum","numeric","best","label","response","error","nodes","broken","detail","start","end","lower","upper","ragged","expected"],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n if (header.kind === \"tree_ensemble\") assertTreesTerminate(header, arrays);\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Prove once, at load, that every tree walk ends inside its arrays.\n *\n * The walk in `treeScores` follows `node_left`/`node_right` until it meets a\n * leaf, with no step limit — a limit there would be paid on every node of every\n * row. So a file whose child index points back up the tree spun the tab forever\n * on the first `predict`, and one pointing past the arrays read `undefined` and\n * answered `NaN`. scikit-learn numbers a node before its children, and\n * `export_sklearn_to_compact` keeps that order with each tree's nodes contiguous,\n * so a well-formed file satisfies `node < child < next tree's offset` everywhere\n * — measured on every tree fixture in `__fixtures__` (1 761 nodes, 0 violations).\n * Checking that invariant is linear in the node count and rules out a cycle by\n * construction, since every step strictly increases the index.\n *\n * @param header The parsed header.\n * @param arrays The sections, by name.\n * @throws {@link CompactFormatError} when a tree could loop, leave its arrays,\n * read a feature the model does not have, or a leaf slot past the values.\n */\nfunction assertTreesTerminate(\n header: CompactHeader,\n arrays: Record<string, Float32Array | Int32Array>,\n): void {\n const feature = arrays.node_feature;\n const left = arrays.node_left;\n const right = arrays.node_right;\n const leaf = arrays.leaf_value;\n const offsets = arrays.tree_offset;\n if (!feature || !left || !right || !leaf || !offsets || offsets.length < 2) {\n throw new CompactFormatError(\"This tree model is missing its node or offset sections.\");\n }\n const nodes = feature.length;\n const outputs = header.n_outputs;\n const broken = (detail: string): CompactFormatError =>\n new CompactFormatError(`This tree model is malformed: ${detail}. Re-export it.`);\n\n if (left.length !== nodes || right.length !== nodes || offsets[0] !== 0) {\n throw broken(\"the node sections disagree on the node count\");\n }\n if (offsets[offsets.length - 1] !== nodes) {\n throw broken(\"the last tree does not end at the last node\");\n }\n for (let tree = 0; tree < offsets.length - 1; tree += 1) {\n const start = offsets[tree] as number;\n const end = offsets[tree + 1] as number;\n if (end <= start) throw broken(`tree ${tree} has no nodes`);\n for (let node = start; node < end; node += 1) {\n const column = feature[node] as number;\n if (column >= 0) {\n const lower = left[node] as number;\n const upper = right[node] as number;\n if (column >= header.n_features) {\n throw broken(`node ${node} splits on feature ${column}`);\n }\n if (lower <= node || upper <= node || lower >= end || upper >= end) {\n throw broken(`node ${node} points outside its tree or back up it`);\n }\n } else if ((-1 - column) * outputs + outputs > leaf.length) {\n throw broken(`leaf ${node} reads past the leaf values`);\n }\n }\n }\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
@@ -8,24 +8,24 @@ var r = "TMC1", i = class t {
8
8
  this.header = e, this.arrays = t, this.info = n;
9
9
  }
10
10
  static async create(n, i) {
11
- let o = await a(n, i), s = new Uint8Array(o);
12
- if (String.fromCharCode(...s.subarray(0, 4)) !== r) throw new e(`This is not a compact model file: it does not start with "${r}". A .onnx file goes through TabularPredictor instead.`);
13
- let c = new DataView(o).getUint32(4, !0), l = JSON.parse(new TextDecoder().decode(s.subarray(8, 8 + c)));
14
- if (l.schema_version > 1) throw new e(`This file uses compact layout ${l.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving it.`);
15
- let u = {}, d = 8 + c;
16
- for (let e of l.sections) u[e.name] = e.dtype === "float32" ? new Float32Array(o, d, e.length) : new Int32Array(o, d, e.length), d += e.length * 4;
17
- return new t(l, u, {
18
- kind: l.kind,
19
- classes: l.classes,
20
- numFeatures: l.n_features,
21
- featureNames: l.feature_names,
22
- numTrees: l.n_trees ?? 0,
23
- isClassifier: l.task === "classification",
24
- estimator: l.estimator
11
+ let s = await a(n, i), c = new Uint8Array(s);
12
+ if (String.fromCharCode(...c.subarray(0, 4)) !== r) throw new e(`This is not a compact model file: it does not start with "${r}". A .onnx file goes through TabularPredictor instead.`);
13
+ let l = new DataView(s).getUint32(4, !0), u = JSON.parse(new TextDecoder().decode(c.subarray(8, 8 + l)));
14
+ if (u.schema_version > 1) throw new e(`This file uses compact layout ${u.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving it.`);
15
+ let d = {}, f = 8 + l;
16
+ for (let e of u.sections) d[e.name] = e.dtype === "float32" ? new Float32Array(s, f, e.length) : new Int32Array(s, f, e.length), f += e.length * 4;
17
+ return u.kind === "tree_ensemble" && o(u, d), new t(u, d, {
18
+ kind: u.kind,
19
+ classes: u.classes,
20
+ numFeatures: u.n_features,
21
+ featureNames: u.feature_names,
22
+ numTrees: u.n_trees ?? 0,
23
+ isClassifier: u.task === "classification",
24
+ estimator: u.estimator
25
25
  });
26
26
  }
27
27
  async predict(e) {
28
- let t = o(e, this.header.n_features), n = performance.now(), r = [];
28
+ let t = s(e, this.header.n_features), n = performance.now(), r = [];
29
29
  for (let n of e) {
30
30
  let e = this.preprocess(n, t);
31
31
  r.push(this.header.kind === "linear" ? this.linearScores(e) : this.treeScores(e));
@@ -108,7 +108,26 @@ async function a(e, t) {
108
108
  }
109
109
  return e instanceof Uint8Array ? e.buffer.slice(e.byteOffset, e.byteOffset + e.byteLength) : e;
110
110
  }
111
- function o(e, n) {
111
+ function o(t, n) {
112
+ let r = n.node_feature, i = n.node_left, a = n.node_right, o = n.leaf_value, s = n.tree_offset;
113
+ if (!r || !i || !a || !o || !s || s.length < 2) throw new e("This tree model is missing its node or offset sections.");
114
+ let c = r.length, l = t.n_outputs, u = (t) => new e(`This tree model is malformed: ${t}. Re-export it.`);
115
+ if (i.length !== c || a.length !== c || s[0] !== 0) throw u("the node sections disagree on the node count");
116
+ if (s[s.length - 1] !== c) throw u("the last tree does not end at the last node");
117
+ for (let e = 0; e < s.length - 1; e += 1) {
118
+ let n = s[e], c = s[e + 1];
119
+ if (c <= n) throw u(`tree ${e} has no nodes`);
120
+ for (let e = n; e < c; e += 1) {
121
+ let n = r[e];
122
+ if (n >= 0) {
123
+ let r = i[e], o = a[e];
124
+ if (n >= t.n_features) throw u(`node ${e} splits on feature ${n}`);
125
+ if (r <= e || o <= e || r >= c || o >= c) throw u(`node ${e} points outside its tree or back up it`);
126
+ } else if ((-1 - n) * l + l > o.length) throw u(`leaf ${e} reads past the leaf values`);
127
+ }
128
+ }
129
+ }
130
+ function s(e, n) {
112
131
  if (!Array.isArray(e) || e.length === 0) throw new t("predict() needs at least one row, shaped [[f1, f2, ...]].");
113
132
  let r = e[0]?.length ?? 0, i = e.findIndex((e) => e.length !== r);
114
133
  if (i !== -1) throw new t(`All rows must have the same width; row ${i} has ${e[i]?.length} values, expected ${r}.`);
@@ -1 +1 @@
1
- {"version":3,"file":"compact.js","names":["MAGIC","CompactPredictor","header","arrays","info","buffer","toBuffer","source","requestInit","bytes","CompactFormatError","headerLength","cursor","section","width","validateRows","rows","started","scores","row","prepared","preprocess","scaled","index","coef","intercept","features","outputs","output","total","base","feature","threshold","left","right","leaf","offsets","trees","totals","tree","node","column","slot","link","classes","probabilities","positive","highest","exponentiated","value","sum","numeric","best","label","response","error","ModelFetchError","FeatureShapeError","ragged","expected"],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":";;AAkCA,IAAMA,IAAQ,QA+DDC,IAAb,MAAaA,EAAiB;CAEL;CACA;CAED;CAJpB,YACI,GACA,GAEA,GACF;EADkB,AAHC,KAAA,SAAAC,GACA,KAAA,SAAAC,GAED,KAAA,OAAAC;CACjB;CAYH,aAAa,OACT,GACA,GACyB;EACzB,IAAMC,IAAS,MAAMC,EAASC,GAAQC,CAAW,GAC3CC,IAAQ,IAAI,WAAWJ,CAAM;EAEnC,IAAI,OAAO,aAAa,GAAGI,EAAM,SAAS,GAAG,CAAC,CAAC,MAAMT,GACjD,MAAM,IAAIU,EACN,6DACQV,EAAM,uDAClB;EAIJ,IAAMW,IAAe,IADJ,SAASN,CACL,CAAA,CAAK,UAAU,GAAG,EAAI,GACrCH,IAAS,KAAK,MAChB,IAAI,YAAY,CAAC,CAAC,OAAOO,EAAM,SAAS,GAAG,IAAIE,CAAY,CAAC,CAChE;EAEA,IAAIT,EAAO,iBAAA,GACP,MAAM,IAAIQ,EACN,iCAAiCR,EAAO,eAAe,2EAG3D;EAGJ,IAAMC,IAAoD,CAAC,GACvDS,IAAS,IAAID;EACjB,KAAK,IAAME,KAAWX,EAAO,UAKzB,AAJA,EAAOW,EAAQ,QACXA,EAAQ,UAAU,YACZ,IAAI,aAAaR,GAAQO,GAAQC,EAAQ,MAAM,IAC/C,IAAI,WAAWR,GAAQO,GAAQC,EAAQ,MAAM,GACvD,KAAUA,EAAQ,SAAS;EAG/B,OAAO,IAAIZ,EAAiBC,GAAQC,GAAQ;GACxC,MAAMD,EAAO;GACb,SAASA,EAAO;GAChB,aAAaA,EAAO;GACpB,cAAcA,EAAO;GACrB,UAAUA,EAAO,WAAW;GAC5B,cAAcA,EAAO,SAAS;GAC9B,WAAWA,EAAO;EACtB,CAAC;CACL;CAYA,MAAM,QAAQ,GAAyD;EACnE,IAAMY,IAAQC,EAAaC,GAAM,KAAK,OAAO,UAAU,GACjDC,IAAU,YAAY,IAAI,GAE1BC,IAAqB,CAAC;EAC5B,KAAK,IAAMC,KAAOH,GAAM;GACpB,IAAMI,IAAW,KAAK,WAAWD,GAAKL,CAAK;GAC3C,EAAO,KACH,KAAK,OAAO,SAAS,WACf,KAAK,aAAaM,CAAQ,IAC1B,KAAK,WAAWA,CAAQ,CAClC;EACJ;EAEA,IAAM,EAAE,WAAQ,qBAAkB,KAAK,OAAOF,CAAM;EACpD,OAAO;GACH;GACA;GACA,SAASF,EAAK;GACd,IAAI,YAAY,IAAI,IAAIC;EAC5B;CACJ;CAGA,MAAM,UAAyB,CAE/B;CASA,WAAmB,GAAiB,GAAyB;EACzD,IAAMI,IAAa,KAAK,OAAO;EAC/B,IAAIA,MAAe,MAAM,OAAOF;EAChC,IAAMG,IAAa,MAAcR,CAAK;EACtC,KAAK,IAAIS,IAAQ,GAAGA,IAAQT,GAAO,KAAS,GACxC,EAAOS,MACDJ,EAAII,KAAqBF,EAAW,OAAOE,MAC5CF,EAAW,MAAME;EAE1B,OAAOD;CACX;CAQA,aAAqB,GAAkC;EACnD,IAAME,IAAO,KAAK,OAAO,MACnBC,IAAY,KAAK,OAAO,WACxBC,IAAW,KAAK,OAAO,YACvBC,IAAU,KAAK,OAAO,WAEtBT,IAAa,MAAcS,CAAO;EACxC,KAAK,IAAIC,IAAS,GAAGA,IAASD,GAAS,KAAU,GAAG;GAChD,IAAIE,IAAQJ,EAAUG,IAChBE,IAAOF,IAASF;GACtB,KAAK,IAAIH,IAAQ,GAAGA,IAAQG,GAAU,KAAS,GAC3C,KAAUF,EAAKM,IAAOP,KAAqBJ,EAAII;GAEnD,EAAOK,KAAUC;EACrB;EACA,OAAOX;CACX;CAmBA,WAAmB,GAAkC;EACjD,IAAMa,IAAU,KAAK,OAAO,cACtBC,IAAY,KAAK,OAAO,gBACxBC,IAAO,KAAK,OAAO,WACnBC,IAAQ,KAAK,OAAO,YACpBC,IAAO,KAAK,OAAO,YACnBC,IAAU,KAAK,OAAO,aACtBT,IAAU,KAAK,OAAO,WACtBU,IAAQD,EAAQ,SAAS,GAEzBE,IAAa,MAAcX,CAAO,CAAC,CAAC,KAAK,CAAC;EAChD,KAAK,IAAIY,IAAO,GAAGA,IAAOF,GAAO,KAAQ,GAAG;GACxC,IAAIG,IAAOJ,EAAQG,IACfE,IAASV,EAAQS;GACrB,OAAOC,KAAU,IAKb,AAJA,IACI,KAAK,OAAOtB,EAAIsB,EAAiB,KAAMT,EAAUQ,KAC1CP,EAAKO,KACLN,EAAMM,IACjB,IAAST,EAAQS;GAErB,IAAME,KAAQ,KAAKD,KAAUd;GAC7B,KAAK,IAAIC,IAAS,GAAGA,IAASD,GAAS,KAAU,GAC7C,EAAOC,MAAWO,EAAKO,IAAOd;EAEtC;EACA,KAAK,IAAIA,IAAS,GAAGA,IAASD,GAAS,KAAU,GAC7C,EAAOC,KAAWU,EAAOV,KAAqBS;EAElD,OAAOC;CACX;CAaA,OAAe,GAGb;EACE,IAAMK,IAAO,KAAK,OAAO,MACnBC,IAAU,KAAK,OAAO;EAE5B,IAAID,MAAS,YACT,OAAO;GAAE,QAAQzB,EAAO,KAAK,MAAQC,EAAI,EAAY;GAAG,eAAe,CAAC;EAAE;EAG9E,IAAM0B,IAAgB3B,EAAO,KAAK,MAAQ;GACtC,IAAIyB,MAAS,WAAW;IACpB,IAAMG,IAAW,KAAK,IAAI,KAAK,IAAI,CAAE3B,EAAI,EAAa;IACtD,OAAO,CAAC,IAAI2B,GAAUA,CAAQ;GAClC;GACA,IAAIH,MAAS,WAAW;IACpB,IAAMI,IAAU,KAAK,IAAI,GAAG5B,CAAG,GACzB6B,IAAgB7B,EAAI,KAAK,MAAU,KAAK,IAAI8B,IAAQF,CAAO,CAAC,GAC5DlB,IAAQmB,EAAc,QAAQ,GAAK,MAAUE,IAAMD,GAAO,CAAC;IACjE,OAAOD,EAAc,KAAK,MAAUC,IAAQpB,CAAK;GACrD;GACA,IAAMA,IAAQV,EAAI,QAAQ,GAAK,MAAU+B,IAAMD,GAAO,CAAC;GACvD,OAAOpB,MAAU,IAAI,CAAC,GAAGV,CAAG,IAAIA,EAAI,KAAK,MAAU8B,IAAQpB,CAAK;EACpE,CAAC,GAEKsB,IAAU,KAAK,OAAO,eAAe;EAW3C,OAAO;GAAE,QAVMN,EAAc,KAAK,MAAQ;IACtC,IAAIO,IAAO;IACX,KAAK,IAAI7B,IAAQ,GAAGA,IAAQJ,EAAI,QAAQ,KAAS,GAC7C,AAAKA,EAAII,KAAqBJ,EAAIiC,OAAkB,IAAO7B;IAE/D,IAAM8B,IAAQT,EAAQQ;IAEtB,OADIC,MAAU,KAAA,IAAkBD,IACzBD,IAAU,OAAOE,CAAK,IAAIA;GACrC,CAES;GAAQ;EAAc;CACnC;AACJ;AAUA,eAAe/C,EACX,GACA,GACoB;CACpB,IAAI,OAAOC,KAAW,UAAU;EAC5B,IAAI+C;EACJ,IAAI;GACA,IAAW,MAAM,MAAM/C,GAAQC,CAAW;EAC9C,SAAS+C,GAAO;GACZ,MAAM,IAAIC,EAAgB,iCAAiCjD,KAAU,EACjE,OAAOgD,EACX,CAAC;EACL;EACA,IAAI,CAACD,EAAS,IACV,MAAM,IAAIE,EACN,iCAAiCF,EAAS,OAAO,GAAGA,EAAS,YACjE;EAEJ,OAAO,MAAMA,EAAS,YAAY;CACtC;CAOA,OANI/C,aAAkB,aACXA,EAAO,OAAO,MACjBA,EAAO,YACPA,EAAO,aAAaA,EAAO,UAC/B,IAEGA;AACX;AAUA,SAASQ,EAAa,GAA6B,GAA0B;CACzE,IAAI,CAAC,MAAM,QAAQC,CAAI,KAAKA,EAAK,WAAW,GACxC,MAAM,IAAIyC,EAAkB,2DAA2D;CAE3F,IAAM3C,IAAQE,EAAK,EAAE,EAAE,UAAU,GAC3B0C,IAAS1C,EAAK,WAAW,MAAQG,EAAI,WAAWL,CAAK;CAC3D,IAAI4C,MAAW,IACX,MAAM,IAAID,EACN,0CAA0CC,EAAO,OAC1C1C,EAAK0C,EAAO,EAAE,OAAO,oBAAoB5C,EAAM,EAC1D;CAEJ,IAAIA,MAAU6C,GACV,MAAM,IAAIF,EACN,qBAAqBE,EAAS,yBAAyB7C,EAAM,EACjE;CAEJ,OAAOA;AACX"}
1
+ {"version":3,"file":"compact.js","names":["MAGIC","CompactPredictor","header","arrays","info","buffer","toBuffer","source","requestInit","bytes","CompactFormatError","headerLength","cursor","section","assertTreesTerminate","width","validateRows","rows","started","scores","row","prepared","preprocess","scaled","index","coef","intercept","features","outputs","output","total","base","feature","threshold","left","right","leaf","offsets","trees","totals","tree","node","column","slot","link","classes","probabilities","positive","highest","exponentiated","value","sum","numeric","best","label","response","error","ModelFetchError","nodes","broken","detail","start","end","lower","upper","FeatureShapeError","ragged","expected"],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n if (header.kind === \"tree_ensemble\") assertTreesTerminate(header, arrays);\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Prove once, at load, that every tree walk ends inside its arrays.\n *\n * The walk in `treeScores` follows `node_left`/`node_right` until it meets a\n * leaf, with no step limit — a limit there would be paid on every node of every\n * row. So a file whose child index points back up the tree spun the tab forever\n * on the first `predict`, and one pointing past the arrays read `undefined` and\n * answered `NaN`. scikit-learn numbers a node before its children, and\n * `export_sklearn_to_compact` keeps that order with each tree's nodes contiguous,\n * so a well-formed file satisfies `node < child < next tree's offset` everywhere\n * — measured on every tree fixture in `__fixtures__` (1 761 nodes, 0 violations).\n * Checking that invariant is linear in the node count and rules out a cycle by\n * construction, since every step strictly increases the index.\n *\n * @param header The parsed header.\n * @param arrays The sections, by name.\n * @throws {@link CompactFormatError} when a tree could loop, leave its arrays,\n * read a feature the model does not have, or a leaf slot past the values.\n */\nfunction assertTreesTerminate(\n header: CompactHeader,\n arrays: Record<string, Float32Array | Int32Array>,\n): void {\n const feature = arrays.node_feature;\n const left = arrays.node_left;\n const right = arrays.node_right;\n const leaf = arrays.leaf_value;\n const offsets = arrays.tree_offset;\n if (!feature || !left || !right || !leaf || !offsets || offsets.length < 2) {\n throw new CompactFormatError(\"This tree model is missing its node or offset sections.\");\n }\n const nodes = feature.length;\n const outputs = header.n_outputs;\n const broken = (detail: string): CompactFormatError =>\n new CompactFormatError(`This tree model is malformed: ${detail}. Re-export it.`);\n\n if (left.length !== nodes || right.length !== nodes || offsets[0] !== 0) {\n throw broken(\"the node sections disagree on the node count\");\n }\n if (offsets[offsets.length - 1] !== nodes) {\n throw broken(\"the last tree does not end at the last node\");\n }\n for (let tree = 0; tree < offsets.length - 1; tree += 1) {\n const start = offsets[tree] as number;\n const end = offsets[tree + 1] as number;\n if (end <= start) throw broken(`tree ${tree} has no nodes`);\n for (let node = start; node < end; node += 1) {\n const column = feature[node] as number;\n if (column >= 0) {\n const lower = left[node] as number;\n const upper = right[node] as number;\n if (column >= header.n_features) {\n throw broken(`node ${node} splits on feature ${column}`);\n }\n if (lower <= node || upper <= node || lower >= end || upper >= end) {\n throw broken(`node ${node} points outside its tree or back up it`);\n }\n } else if ((-1 - column) * outputs + outputs > leaf.length) {\n throw broken(`leaf ${node} reads past the leaf values`);\n }\n }\n }\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("./exceptions.cjs"),t=require("./cache.cjs"),n=require("./compact.cjs"),r=require("./predictor.cjs");var i=`manifest.json`;async function a(t,n){let r=l(t),i;try{i=await fetch(r,n)}catch(t){throw new e.ModelFetchError(`Could not read the manifest at ${r}`,{cause:t})}if(!i.ok)throw new e.ModelFetchError(`Could not read the manifest at ${r}: ${i.status} ${i.statusText}`);let a=await i.json();if(typeof a?.schema_version!=`number`)throw new e.ModelFetchError(`${r} is not an edge package manifest (no schema_version).`);if(a.schema_version>1)throw new e.ModelFetchError(`The manifest at ${r} uses schema_version ${a.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving this package.`);return a}async function o(e,i={}){let o=await a(e),l=e.endsWith(`/`)?e:`${e}/`,u=s(o,i.runtime??`auto`),d=`${l}${c(o,u)}`,f=i.cache??!0,p=f===!1?d:await t.fetchModelBytes(d,typeof f==`object`?f:{}),m=u===`compact`?await n.CompactPredictor.create(p):await r.TabularPredictor.create(p,{providers:i.providers,warmup:i.warmup,sessionOptions:i.sessionOptions}),h=o.output.classes;return{manifest:o,predictor:m,runtime:u,featureNames:o.input.feature_names,classes:h,explain:e=>e.map((e,t)=>({name:h[t]??String(t),score:e})).sort((e,t)=>t.score-e.score)}}function s(t,n){let r=t.runtimes??[],i=r.some(e=>e.kind===`compact`);if(n===`auto`)return i?`compact`:`onnx`;if(n===`compact`&&!i)throw new e.ModelFetchError(`This package carries no compact model (${t.name} lists ${r.map(e=>e.kind).join(`, `)||`onnx`}). Re-export it with edge_pipeline(compact=True), or load it as ONNX.`);return n}function c(e,t){return(e.runtimes??[]).find(e=>e.kind===t)?.file??e.model.file}function l(e){return e.endsWith(`.json`)?e:e.endsWith(`/`)?`${e}${i}`:`${e}/${i}`}exports.MANIFEST_FILENAME=i,exports.fetchEdgeManifest=a,exports.loadEdgePackage=o;
1
+ const e=require("./exceptions.cjs"),t=require("./cache.cjs"),n=require("./compact.cjs"),r=require("./predictor.cjs");var i=`manifest.json`;async function a(t,n){let r=d(t),i;try{i=await fetch(r,n)}catch(t){throw new e.ModelFetchError(`Could not read the manifest at ${r}`,{cause:t})}if(!i.ok)throw new e.ModelFetchError(`Could not read the manifest at ${r}: ${i.status} ${i.statusText}`);let a=await i.json();if(typeof a?.schema_version!=`number`)throw new e.ModelFetchError(`${r} is not an edge package manifest (no schema_version).`);if(a.schema_version>1)throw new e.ModelFetchError(`The manifest at ${r} uses schema_version ${a.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving this package.`);return a}async function o(e,t={}){let i=await a(e),o=e.endsWith(`/`)?e:`${e}/`,l=s(i,t.runtime??`auto`),{file:d,sha256:f}=c(i,l),p=`${o}${d}`,m=t.cache??!0,h=m===!1?p:await u(p,f,typeof m==`object`?m:{}),g=l===`compact`?await n.CompactPredictor.create(h):await r.TabularPredictor.create(h,{providers:t.providers,warmup:t.warmup,sessionOptions:t.sessionOptions}),_=i.output.classes;return{manifest:i,predictor:g,runtime:l,featureNames:i.input.feature_names,classes:_,explain:e=>e.map((e,t)=>({name:_[t]??String(t),score:e})).sort((e,t)=>t.score-e.score)}}function s(t,n){let r=t.runtimes??[],i=r.some(e=>e.kind===`compact`);if(n===`auto`)return i?`compact`:`onnx`;if(n===`compact`&&!i)throw new e.ModelFetchError(`This package carries no compact model (${t.name} lists ${r.map(e=>e.kind).join(`, `)||`onnx`}). Re-export it with edge_pipeline(compact=True), or load it as ONNX.`);return n}function c(e,t){return(e.runtimes??[]).find(e=>e.kind===t)??e.model}async function l(e,t){let n=globalThis.crypto?.subtle;if(!n||!t)return!0;let r=new Uint8Array(await n.digest(`SHA-256`,e));return Array.from(r,e=>e.toString(16).padStart(2,`0`)).join(``)===t.toLowerCase()}async function u(n,r,i){let a=await t.fetchModelBytes(n,i);if(await l(a,r))return a;if(i.revalidate!==!0){let e=await t.fetchModelBytes(n,{...i,revalidate:!0});if(await l(e,r))return e}throw new e.ModelFetchError(`The model at ${n} does not match the sha256 in its manifest. The file and the manifest were published separately, or the download was altered — republish the package so both describe the same model.`)}function d(e){return e.endsWith(`.json`)?e:e.endsWith(`/`)?`${e}${i}`:`${e}/${i}`}exports.MANIFEST_FILENAME=i,exports.fetchEdgeManifest=a,exports.loadEdgePackage=o;
2
2
  //# sourceMappingURL=manifest.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.cjs","names":["ModelFetchError","fetchModelBytes","CompactPredictor","TabularPredictor","require_exceptions","require_cache","require_compact","require_predictor","MANIFEST_FILENAME","fetchEdgeManifest","url","manifestUrl","directoryUrl","response","requestInit","error","manifest","loadEdgePackage","base","runtime","chooseRuntime","options","modelUrl","fileFor","cache","source","predictor","classes","probabilities","index","b","a","available","hasCompact","entry","requested","item"],"sources":["../../src/tabular/manifest.ts"],"sourcesContent":["/**\n * Reading an edge package published by `tempest-fastapi-sdk`.\n *\n * `edge_pipeline` writes a directory — the graph, a gzipped copy, a drift\n * baseline and a `manifest.json` — and the browser is one of its two\n * intended consumers. The manifest is what turns a `.onnx` URL into\n * something a UI can use: the **column order** the model was trained on,\n * the class names behind `probabilities[2]`, and a version to compare\n * against what is already cached.\n *\n * That column order is the field worth loading a manifest for. A model fed\n * the right features in the wrong order answers confidently and wrongly,\n * and no runtime check catches it.\n *\n * The contract is pinned by `schema_version`. Unknown fields are ignored\n * rather than rejected, so a package written by a newer SDK still loads.\n */\n\nimport { fetchModelBytes, type ModelCacheOptions } from \"./cache\";\nimport { CompactPredictor } from \"./compact\";\nimport { ModelFetchError } from \"./exceptions\";\nimport { TabularPredictor } from \"./predictor\";\nimport type { FeatureRow, TabularPrediction, TabularPredictorOptions } from \"./types\";\n\n/** Manifest schema version this reader was written against. */\nexport const SUPPORTED_MANIFEST_SCHEMA = 1;\n\n/** Fixed filename inside a package directory. */\nexport const MANIFEST_FILENAME = \"manifest.json\";\n\n/** The graph file and how to check you got it whole. */\nexport interface ManifestModelFile {\n readonly file: string;\n readonly sha256: string;\n readonly bytes: number;\n readonly gzip_file: string | null;\n readonly gzip_bytes: number | null;\n readonly opset: number;\n readonly dtype: string;\n}\n\n/** What the graph expects per row. */\nexport interface ManifestInput {\n readonly name: string;\n readonly features: number;\n /** Column order used at training time. */\n readonly feature_names: readonly string[];\n}\n\n/** What the graph answers. */\nexport interface ManifestOutput {\n readonly is_classifier: boolean;\n readonly label_output: string;\n readonly probability_output: string | null;\n /** Class labels in score-column order. */\n readonly classes: readonly string[];\n}\n\n/**\n * Where the packaged model came from, when it came from an existing\n * artifact.\n *\n * Present when the package was built with `edge_pipeline_from_pickle`: the\n * `.pkl` never reaches the browser (a pickle is a Python program, not\n * data), but its name and digest travel in the manifest, so a model\n * answering in a tab can be traced back to the file that produced it.\n */\nexport interface ManifestSource {\n readonly file: string;\n readonly kind: string;\n readonly sha256: string;\n readonly bytes: number;\n readonly sklearn_version: string;\n readonly warnings: readonly string[];\n}\n\n/**\n * One file in the package a runtime can load.\n *\n * A package may carry the same model twice — as ONNX, which any runtime\n * reads at the cost of a 25.6 MB WebAssembly download, and as the compact\n * format, which needs no runtime. The list is what lets the browser pick\n * by what it already ships.\n */\nexport interface ManifestRuntime {\n readonly kind: \"onnx\" | \"compact\" | string;\n readonly file: string;\n readonly bytes: number;\n readonly gzip_file: string | null;\n readonly gzip_bytes: number | null;\n readonly sha256: string;\n}\n\n/** The package manifest, as written by `edge_pipeline`. */\nexport interface EdgeManifest {\n readonly schema_version: number;\n readonly name: string;\n readonly version: string;\n readonly created_at: string;\n readonly sdk_version: string;\n readonly estimator: string;\n readonly model: ManifestModelFile;\n readonly input: ManifestInput;\n readonly output: ManifestOutput;\n readonly verified: boolean | null;\n /** Every file a runtime can load. Absent on packages written before v0.194. */\n readonly runtimes?: readonly ManifestRuntime[];\n /** Absent on packages built straight from a fitted estimator. */\n readonly source?: ManifestSource;\n readonly baseline_file: string | null;\n readonly baseline_samples: number;\n}\n\n/** Which reader served the package. */\nexport type TabularRuntime = \"onnx\" | \"compact\";\n\n/** A package loaded and ready to answer. */\nexport interface LoadedEdgePackage {\n /** What was published. */\n readonly manifest: EdgeManifest;\n /** The running model, whichever runtime read it. */\n readonly predictor: PredictorLike;\n /** Which reader was used. */\n readonly runtime: TabularRuntime;\n /** Column order the rows must follow. */\n readonly featureNames: readonly string[];\n /** Class names behind each probability column. */\n readonly classes: readonly string[];\n /**\n * Map a prediction's scores onto class names.\n *\n * @param probabilities One row of scores.\n * @returns Name/score pairs, highest first.\n */\n readonly explain: (probabilities: readonly number[]) => { name: string; score: number }[];\n}\n\n/**\n * The shape both readers share.\n *\n * `TabularPredictor` (ONNX) and `CompactPredictor` (runtime-free) answer\n * with the same object, so an app can switch routes without touching a\n * line of its own code.\n */\nexport interface PredictorLike {\n predict(rows: readonly FeatureRow[]): Promise<TabularPrediction>;\n dispose(): Promise<void>;\n}\n\n/** Options for {@link loadEdgePackage}. */\nexport interface LoadEdgePackageOptions extends TabularPredictorOptions {\n /** Cache the model bytes for offline use. `true` by default. */\n readonly cache?: boolean | ModelCacheOptions;\n /**\n * Which reader to use.\n *\n * `\"auto\"` (the default) takes the compact form when the package has\n * one, because it answers without downloading a WebAssembly runtime.\n * Force `\"onnx\"` when the app already ships ONNX for something else —\n * then the runtime is already paid for and ONNX covers more estimators.\n */\n readonly runtime?: TabularRuntime | \"auto\";\n}\n\n/**\n * Read a package's manifest.\n *\n * Cheap: it is a few hundred bytes, so an app can check for a new version\n * without downloading a model it may already have.\n *\n * @example\n * ```ts\n * const manifest = await fetchEdgeManifest(\"/models/risk/\");\n * if (manifest.version !== localStorage.getItem(\"risk-version\")) {\n * // a new model was published\n * }\n * ```\n *\n * @param directoryUrl URL of the package directory, with or without a\n * trailing slash. A full URL to the manifest file also works.\n * @param requestInit `fetch` options.\n * @returns The parsed manifest.\n * @throws {@link ModelFetchError} when the manifest cannot be read, or when\n * its `schema_version` is newer than this reader understands — loading it\n * anyway would risk misreading the field that defines column order.\n */\nexport async function fetchEdgeManifest(\n directoryUrl: string,\n requestInit?: RequestInit,\n): Promise<EdgeManifest> {\n const url = manifestUrl(directoryUrl);\n let response: Response;\n try {\n response = await fetch(url, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not read the manifest at ${url}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not read the manifest at ${url}: ${response.status} ${response.statusText}`,\n );\n }\n\n const manifest = (await response.json()) as EdgeManifest;\n if (typeof manifest?.schema_version !== \"number\") {\n throw new ModelFetchError(`${url} is not an edge package manifest (no schema_version).`);\n }\n if (manifest.schema_version > SUPPORTED_MANIFEST_SCHEMA) {\n throw new ModelFetchError(\n `The manifest at ${url} uses schema_version ${manifest.schema_version}, ` +\n `and this SDK understands ${SUPPORTED_MANIFEST_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving this package.\",\n );\n }\n return manifest;\n}\n\n/**\n * Load a whole edge package: manifest, model, and the names to read it by.\n *\n * @example\n * ```ts\n * const pkg = await loadEdgePackage(\"/models/risk/\");\n *\n * console.log(pkg.featureNames); // [\"age\", \"income\", \"tenure\", \"score\", \"visits\"]\n *\n * const { probabilities } = await pkg.predictor.predict([[41, 5200, 3, 0.82, 12]]);\n * console.log(pkg.explain(probabilities[0]!)); // [{ name: \"approved\", score: 0.91 }, ...]\n * ```\n *\n * @param directoryUrl URL of the package directory.\n * @param options Predictor options plus caching.\n * @returns The loaded package.\n * @throws {@link ModelFetchError} when the manifest or model cannot be read.\n */\nexport async function loadEdgePackage(\n directoryUrl: string,\n options: LoadEdgePackageOptions = {},\n): Promise<LoadedEdgePackage> {\n const manifest = await fetchEdgeManifest(directoryUrl);\n const base = directoryUrl.endsWith(\"/\") ? directoryUrl : `${directoryUrl}/`;\n const runtime = chooseRuntime(manifest, options.runtime ?? \"auto\");\n const file = fileFor(manifest, runtime);\n const modelUrl = `${base}${file}`;\n\n const cache = options.cache ?? true;\n const source =\n cache === false\n ? modelUrl\n : await fetchModelBytes(modelUrl, typeof cache === \"object\" ? cache : {});\n\n const predictor: PredictorLike =\n runtime === \"compact\"\n ? await CompactPredictor.create(source)\n : await TabularPredictor.create(source, {\n providers: options.providers,\n warmup: options.warmup,\n sessionOptions: options.sessionOptions,\n });\n\n const classes = manifest.output.classes;\n return {\n manifest,\n predictor,\n runtime,\n featureNames: manifest.input.feature_names,\n classes,\n explain: (probabilities: readonly number[]) =>\n probabilities\n .map((score, index) => ({\n name: classes[index] ?? String(index),\n score,\n }))\n .sort((a, b) => b.score - a.score),\n };\n}\n\n/**\n * Decide which reader serves this package.\n *\n * @param manifest The package manifest.\n * @param requested What the caller asked for.\n * @returns The runtime to use.\n * @throws {@link ModelFetchError} when the package does not carry the\n * requested form — asking for a compact model that was never written\n * should say so, not silently download 25 MB of WebAssembly instead.\n */\nfunction chooseRuntime(manifest: EdgeManifest, requested: TabularRuntime | \"auto\"): TabularRuntime {\n const available = manifest.runtimes ?? [];\n const hasCompact = available.some((entry) => entry.kind === \"compact\");\n\n if (requested === \"auto\") return hasCompact ? \"compact\" : \"onnx\";\n if (requested === \"compact\" && !hasCompact) {\n throw new ModelFetchError(\n `This package carries no compact model (${manifest.name} lists ` +\n `${available.map((entry) => entry.kind).join(\", \") || \"onnx\"}). ` +\n \"Re-export it with edge_pipeline(compact=True), or load it as ONNX.\",\n );\n }\n return requested;\n}\n\n/**\n * Find the file a runtime reads.\n *\n * @param manifest The package manifest.\n * @param runtime The chosen runtime.\n * @returns The filename inside the package directory.\n */\nfunction fileFor(manifest: EdgeManifest, runtime: TabularRuntime): string {\n const entry = (manifest.runtimes ?? []).find((item) => item.kind === runtime);\n return entry?.file ?? manifest.model.file;\n}\n\n/**\n * Resolve a directory URL to its manifest file.\n *\n * @param directoryUrl The package directory, or the manifest itself.\n * @returns The manifest URL.\n */\nfunction manifestUrl(directoryUrl: string): string {\n if (directoryUrl.endsWith(\".json\")) return directoryUrl;\n return directoryUrl.endsWith(\"/\")\n ? `${directoryUrl}${MANIFEST_FILENAME}`\n : `${directoryUrl}/${MANIFEST_FILENAME}`;\n}\n"],"mappings":"A,qHA4BA,IAAaQ,EAAoB,gBA8JjC,eAAsBC,EAClB,EACA,EACqB,CACrB,IAAMC,EAAMC,EAAYC,CAAY,EAChCC,EACJ,GAAI,CACA,EAAW,MAAM,MAAMH,EAAKI,CAAW,CAC3C,OAASC,EAAO,CACZ,MAAM,IAAIf,EAAAA,gBAAgB,kCAAkCU,IAAO,CAC/D,MAAOK,CACX,CAAC,CACL,CACA,GAAI,CAACF,EAAS,GACV,MAAM,IAAIb,EAAAA,gBACN,kCAAkCU,EAAI,IAAIG,EAAS,OAAO,GAAGA,EAAS,YAC1E,EAGJ,IAAMG,EAAY,MAAMH,EAAS,KAAK,EACtC,GAAI,OAAOG,GAAU,gBAAmB,SACpC,MAAM,IAAIhB,EAAAA,gBAAgB,GAAGU,EAAI,sDAAsD,EAE3F,GAAIM,EAAS,eAAA,EACT,MAAM,IAAIhB,EAAAA,gBACN,mBAAmBU,EAAI,uBAAuBM,EAAS,eAAe,qFAG1E,EAEJ,OAAOA,CACX,CAoBA,eAAsBC,EAClB,EACA,EAAkC,CAAC,EACT,CAC1B,IAAMD,EAAW,MAAMP,EAAkBG,CAAY,EAC/CM,EAAON,EAAa,SAAS,GAAG,EAAIA,EAAe,GAAGA,EAAa,GACnEO,EAAUC,EAAcJ,EAAUK,EAAQ,SAAW,MAAM,EAE3DC,EAAW,GAAGJ,IADPK,EAAQP,EAAUG,CACJ,IAErBK,EAAQH,EAAQ,OAAS,GACzBI,EACFD,IAAU,GACJF,EACA,MAAMrB,EAAAA,gBAAgBqB,EAAU,OAAOE,GAAU,SAAWA,EAAQ,CAAC,CAAC,EAE1EE,EACFP,IAAY,UACN,MAAMjB,EAAAA,iBAAiB,OAAOuB,CAAM,EACpC,MAAMtB,EAAAA,iBAAiB,OAAOsB,EAAQ,CAClC,UAAWJ,EAAQ,UACnB,OAAQA,EAAQ,OAChB,eAAgBA,EAAQ,cAC5B,CAAC,EAELM,EAAUX,EAAS,OAAO,QAChC,MAAO,CACH,WACA,YACA,UACA,aAAcA,EAAS,MAAM,cAC7B,UACA,QAAU,GACNY,EACK,KAAK,EAAO,KAAW,CACpB,KAAMD,EAAQE,IAAU,OAAOA,CAAK,EACpC,OACJ,EAAE,CAAC,CACF,MAAM,EAAG,IAAMC,EAAE,MAAQC,EAAE,KAAK,CAC7C,CACJ,CAYA,SAASX,EAAc,EAAwB,EAAoD,CAC/F,IAAMY,EAAYhB,EAAS,UAAY,CAAC,EAClCiB,EAAaD,EAAU,KAAM,GAAUE,EAAM,OAAS,SAAS,EAErE,GAAIC,IAAc,OAAQ,OAAOF,EAAa,UAAY,OAC1D,GAAIE,IAAc,WAAa,CAACF,EAC5B,MAAM,IAAIjC,EAAAA,gBACN,0CAA0CgB,EAAS,KAAK,SACjDgB,EAAU,IAAK,GAAUE,EAAM,IAAI,CAAC,CAAC,KAAK,IAAI,GAAK,OAAO,sEAErE,EAEJ,OAAOC,CACX,CASA,SAASZ,EAAQ,EAAwB,EAAiC,CAEtE,OADeP,EAAS,UAAY,CAAC,EAAA,CAAG,KAAM,GAASoB,EAAK,OAASjB,CAC9D,CAAA,EAAO,MAAQH,EAAS,MAAM,IACzC,CAQA,SAASL,EAAY,EAA8B,CAE/C,OADIC,EAAa,SAAS,OAAO,EAAUA,EACpCA,EAAa,SAAS,GAAG,EAC1B,GAAGA,IAAeJ,IAClB,GAAGI,EAAa,GAAGJ,GAC7B,C"}
1
+ {"version":3,"file":"manifest.cjs","names":["ModelFetchError","CompactPredictor","TabularPredictor","fetchModelBytes","require_exceptions","require_cache","require_compact","require_predictor","MANIFEST_FILENAME","fetchEdgeManifest","url","manifestUrl","directoryUrl","response","requestInit","error","manifest","loadEdgePackage","base","runtime","chooseRuntime","options","fileFor","modelUrl","file","cache","source","verifiedModelBytes","sha256","predictor","classes","probabilities","index","b","a","available","hasCompact","entry","requested","item","digestMatches","subtle","expected","digest","bytes","byte","fresh"],"sources":["../../src/tabular/manifest.ts"],"sourcesContent":["/**\n * Reading an edge package published by `tempest-fastapi-sdk`.\n *\n * `edge_pipeline` writes a directory — the graph, a gzipped copy, a drift\n * baseline and a `manifest.json` — and the browser is one of its two\n * intended consumers. The manifest is what turns a `.onnx` URL into\n * something a UI can use: the **column order** the model was trained on,\n * the class names behind `probabilities[2]`, and a version to compare\n * against what is already cached.\n *\n * That column order is the field worth loading a manifest for. A model fed\n * the right features in the wrong order answers confidently and wrongly,\n * and no runtime check catches it.\n *\n * The contract is pinned by `schema_version`. Unknown fields are ignored\n * rather than rejected, so a package written by a newer SDK still loads.\n */\n\nimport { fetchModelBytes, type ModelCacheOptions } from \"./cache\";\nimport { CompactPredictor } from \"./compact\";\nimport { ModelFetchError } from \"./exceptions\";\nimport { TabularPredictor } from \"./predictor\";\nimport type { FeatureRow, TabularPrediction, TabularPredictorOptions } from \"./types\";\n\n/** Manifest schema version this reader was written against. */\nexport const SUPPORTED_MANIFEST_SCHEMA = 1;\n\n/** Fixed filename inside a package directory. */\nexport const MANIFEST_FILENAME = \"manifest.json\";\n\n/** The graph file and how to check you got it whole. */\nexport interface ManifestModelFile {\n readonly file: string;\n readonly sha256: string;\n readonly bytes: number;\n readonly gzip_file: string | null;\n readonly gzip_bytes: number | null;\n readonly opset: number;\n readonly dtype: string;\n}\n\n/** What the graph expects per row. */\nexport interface ManifestInput {\n readonly name: string;\n readonly features: number;\n /** Column order used at training time. */\n readonly feature_names: readonly string[];\n}\n\n/** What the graph answers. */\nexport interface ManifestOutput {\n readonly is_classifier: boolean;\n readonly label_output: string;\n readonly probability_output: string | null;\n /** Class labels in score-column order. */\n readonly classes: readonly string[];\n}\n\n/**\n * Where the packaged model came from, when it came from an existing\n * artifact.\n *\n * Present when the package was built with `edge_pipeline_from_pickle`: the\n * `.pkl` never reaches the browser (a pickle is a Python program, not\n * data), but its name and digest travel in the manifest, so a model\n * answering in a tab can be traced back to the file that produced it.\n */\nexport interface ManifestSource {\n readonly file: string;\n readonly kind: string;\n readonly sha256: string;\n readonly bytes: number;\n readonly sklearn_version: string;\n readonly warnings: readonly string[];\n}\n\n/**\n * One file in the package a runtime can load.\n *\n * A package may carry the same model twice — as ONNX, which any runtime\n * reads at the cost of a 25.6 MB WebAssembly download, and as the compact\n * format, which needs no runtime. The list is what lets the browser pick\n * by what it already ships.\n */\nexport interface ManifestRuntime {\n readonly kind: \"onnx\" | \"compact\" | string;\n readonly file: string;\n readonly bytes: number;\n readonly gzip_file: string | null;\n readonly gzip_bytes: number | null;\n readonly sha256: string;\n}\n\n/** The package manifest, as written by `edge_pipeline`. */\nexport interface EdgeManifest {\n readonly schema_version: number;\n readonly name: string;\n readonly version: string;\n readonly created_at: string;\n readonly sdk_version: string;\n readonly estimator: string;\n readonly model: ManifestModelFile;\n readonly input: ManifestInput;\n readonly output: ManifestOutput;\n readonly verified: boolean | null;\n /** Every file a runtime can load. Absent on packages written before v0.194. */\n readonly runtimes?: readonly ManifestRuntime[];\n /** Absent on packages built straight from a fitted estimator. */\n readonly source?: ManifestSource;\n readonly baseline_file: string | null;\n readonly baseline_samples: number;\n}\n\n/** Which reader served the package. */\nexport type TabularRuntime = \"onnx\" | \"compact\";\n\n/** A package loaded and ready to answer. */\nexport interface LoadedEdgePackage {\n /** What was published. */\n readonly manifest: EdgeManifest;\n /** The running model, whichever runtime read it. */\n readonly predictor: PredictorLike;\n /** Which reader was used. */\n readonly runtime: TabularRuntime;\n /** Column order the rows must follow. */\n readonly featureNames: readonly string[];\n /** Class names behind each probability column. */\n readonly classes: readonly string[];\n /**\n * Map a prediction's scores onto class names.\n *\n * @param probabilities One row of scores.\n * @returns Name/score pairs, highest first.\n */\n readonly explain: (probabilities: readonly number[]) => { name: string; score: number }[];\n}\n\n/**\n * The shape both readers share.\n *\n * `TabularPredictor` (ONNX) and `CompactPredictor` (runtime-free) answer\n * with the same object, so an app can switch routes without touching a\n * line of its own code.\n */\nexport interface PredictorLike {\n predict(rows: readonly FeatureRow[]): Promise<TabularPrediction>;\n dispose(): Promise<void>;\n}\n\n/** Options for {@link loadEdgePackage}. */\nexport interface LoadEdgePackageOptions extends TabularPredictorOptions {\n /** Cache the model bytes for offline use. `true` by default. */\n readonly cache?: boolean | ModelCacheOptions;\n /**\n * Which reader to use.\n *\n * `\"auto\"` (the default) takes the compact form when the package has\n * one, because it answers without downloading a WebAssembly runtime.\n * Force `\"onnx\"` when the app already ships ONNX for something else —\n * then the runtime is already paid for and ONNX covers more estimators.\n */\n readonly runtime?: TabularRuntime | \"auto\";\n}\n\n/**\n * Read a package's manifest.\n *\n * Cheap: it is a few hundred bytes, so an app can check for a new version\n * without downloading a model it may already have.\n *\n * @example\n * ```ts\n * const manifest = await fetchEdgeManifest(\"/models/risk/\");\n * if (manifest.version !== localStorage.getItem(\"risk-version\")) {\n * // a new model was published\n * }\n * ```\n *\n * @param directoryUrl URL of the package directory, with or without a\n * trailing slash. A full URL to the manifest file also works.\n * @param requestInit `fetch` options.\n * @returns The parsed manifest.\n * @throws {@link ModelFetchError} when the manifest cannot be read, or when\n * its `schema_version` is newer than this reader understands — loading it\n * anyway would risk misreading the field that defines column order.\n */\nexport async function fetchEdgeManifest(\n directoryUrl: string,\n requestInit?: RequestInit,\n): Promise<EdgeManifest> {\n const url = manifestUrl(directoryUrl);\n let response: Response;\n try {\n response = await fetch(url, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not read the manifest at ${url}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not read the manifest at ${url}: ${response.status} ${response.statusText}`,\n );\n }\n\n const manifest = (await response.json()) as EdgeManifest;\n if (typeof manifest?.schema_version !== \"number\") {\n throw new ModelFetchError(`${url} is not an edge package manifest (no schema_version).`);\n }\n if (manifest.schema_version > SUPPORTED_MANIFEST_SCHEMA) {\n throw new ModelFetchError(\n `The manifest at ${url} uses schema_version ${manifest.schema_version}, ` +\n `and this SDK understands ${SUPPORTED_MANIFEST_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving this package.\",\n );\n }\n return manifest;\n}\n\n/**\n * Load a whole edge package: manifest, model, and the names to read it by.\n *\n * @example\n * ```ts\n * const pkg = await loadEdgePackage(\"/models/risk/\");\n *\n * console.log(pkg.featureNames); // [\"age\", \"income\", \"tenure\", \"score\", \"visits\"]\n *\n * const { probabilities } = await pkg.predictor.predict([[41, 5200, 3, 0.82, 12]]);\n * console.log(pkg.explain(probabilities[0]!)); // [{ name: \"approved\", score: 0.91 }, ...]\n * ```\n *\n * The model bytes are checked against the `sha256` the manifest lists for\n * them before any runtime sees them. The cache is keyed by URL and read\n * cache-first, so a model republished in place under the same file name kept\n * answering from the old bytes next to the new manifest — new column order,\n * old weights, no error. A mismatch now refetches once from the network, and a\n * second mismatch throws. Two cases are not checked: `cache: false`, where the\n * runtime fetches the URL itself and the bytes never pass through here, and a\n * non-secure context, which has no `crypto.subtle` (nor Cache Storage).\n *\n * @param directoryUrl URL of the package directory.\n * @param options Predictor options plus caching.\n * @returns The loaded package.\n * @throws {@link ModelFetchError} when the manifest or model cannot be read, or\n * when the model does not match the manifest's `sha256` even after a refetch.\n */\nexport async function loadEdgePackage(\n directoryUrl: string,\n options: LoadEdgePackageOptions = {},\n): Promise<LoadedEdgePackage> {\n const manifest = await fetchEdgeManifest(directoryUrl);\n const base = directoryUrl.endsWith(\"/\") ? directoryUrl : `${directoryUrl}/`;\n const runtime = chooseRuntime(manifest, options.runtime ?? \"auto\");\n const { file, sha256 } = fileFor(manifest, runtime);\n const modelUrl = `${base}${file}`;\n\n const cache = options.cache ?? true;\n const source =\n cache === false\n ? modelUrl\n : await verifiedModelBytes(modelUrl, sha256, typeof cache === \"object\" ? cache : {});\n\n const predictor: PredictorLike =\n runtime === \"compact\"\n ? await CompactPredictor.create(source)\n : await TabularPredictor.create(source, {\n providers: options.providers,\n warmup: options.warmup,\n sessionOptions: options.sessionOptions,\n });\n\n const classes = manifest.output.classes;\n return {\n manifest,\n predictor,\n runtime,\n featureNames: manifest.input.feature_names,\n classes,\n explain: (probabilities: readonly number[]) =>\n probabilities\n .map((score, index) => ({\n name: classes[index] ?? String(index),\n score,\n }))\n .sort((a, b) => b.score - a.score),\n };\n}\n\n/**\n * Decide which reader serves this package.\n *\n * @param manifest The package manifest.\n * @param requested What the caller asked for.\n * @returns The runtime to use.\n * @throws {@link ModelFetchError} when the package does not carry the\n * requested form — asking for a compact model that was never written\n * should say so, not silently download 25 MB of WebAssembly instead.\n */\nfunction chooseRuntime(manifest: EdgeManifest, requested: TabularRuntime | \"auto\"): TabularRuntime {\n const available = manifest.runtimes ?? [];\n const hasCompact = available.some((entry) => entry.kind === \"compact\");\n\n if (requested === \"auto\") return hasCompact ? \"compact\" : \"onnx\";\n if (requested === \"compact\" && !hasCompact) {\n throw new ModelFetchError(\n `This package carries no compact model (${manifest.name} lists ` +\n `${available.map((entry) => entry.kind).join(\", \") || \"onnx\"}). ` +\n \"Re-export it with edge_pipeline(compact=True), or load it as ONNX.\",\n );\n }\n return requested;\n}\n\n/**\n * Find the file a runtime reads, with the digest the manifest records for it.\n *\n * @param manifest The package manifest.\n * @param runtime The chosen runtime.\n * @returns The filename inside the package directory and its `sha256`.\n */\nfunction fileFor(\n manifest: EdgeManifest,\n runtime: TabularRuntime,\n): { file: string; sha256: string } {\n const entry = (manifest.runtimes ?? []).find((item) => item.kind === runtime);\n return entry ?? manifest.model;\n}\n\n/**\n * Whether `bytes` hash to `expected`.\n *\n * Answers `true` when nothing can be compared — no digest in the manifest, or\n * no `crypto.subtle` outside a secure context — because refusing to load there\n * would take inference away from an app over a check it never had.\n *\n * @param bytes The model bytes.\n * @param expected Hex SHA-256 from the manifest.\n * @returns `false` only on a real mismatch.\n */\nasync function digestMatches(bytes: Uint8Array, expected: string): Promise<boolean> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle || !expected) return true;\n const digest = new Uint8Array(await subtle.digest(\"SHA-256\", bytes as Uint8Array<ArrayBuffer>));\n const hex = Array.from(digest, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n return hex === expected.toLowerCase();\n}\n\n/**\n * Read the model bytes and prove they are the ones the manifest describes.\n *\n * A cached copy that does not match is refetched from the network once (the\n * refetch also replaces the cache entry); only a second mismatch throws.\n *\n * @param url The model URL.\n * @param expected Hex SHA-256 from the manifest.\n * @param options Cache options as the caller gave them.\n * @returns The verified bytes.\n * @throws {@link ModelFetchError} when the bytes cannot be read or never match.\n */\nasync function verifiedModelBytes(\n url: string,\n expected: string,\n options: ModelCacheOptions,\n): Promise<Uint8Array> {\n const bytes = await fetchModelBytes(url, options);\n if (await digestMatches(bytes, expected)) return bytes;\n if (options.revalidate !== true) {\n const fresh = await fetchModelBytes(url, { ...options, revalidate: true });\n if (await digestMatches(fresh, expected)) return fresh;\n }\n throw new ModelFetchError(\n `The model at ${url} does not match the sha256 in its manifest. The file and ` +\n \"the manifest were published separately, or the download was altered — \" +\n \"republish the package so both describe the same model.\",\n );\n}\n\n/**\n * Resolve a directory URL to its manifest file.\n *\n * @param directoryUrl The package directory, or the manifest itself.\n * @returns The manifest URL.\n */\nfunction manifestUrl(directoryUrl: string): string {\n if (directoryUrl.endsWith(\".json\")) return directoryUrl;\n return directoryUrl.endsWith(\"/\")\n ? `${directoryUrl}${MANIFEST_FILENAME}`\n : `${directoryUrl}/${MANIFEST_FILENAME}`;\n}\n"],"mappings":"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"}