@winkjs/composer 0.0.2 → 0.4.1

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 (451) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -80
  3. package/package.json +56 -28
  4. package/src/composer.js +75 -0
  5. package/src/core/batch/aoo-encoder.js +148 -0
  6. package/src/core/batch/index.js +12 -0
  7. package/src/core/batch/sliding-window.js +91 -0
  8. package/src/core/codec/index.js +137 -0
  9. package/src/core/emitter-manager/mqtt/constants.js +102 -0
  10. package/src/core/emitter-manager/mqtt/emitter.js +780 -0
  11. package/src/core/emitter-manager/mqtt/index.js +190 -0
  12. package/src/core/emitter-manager/mqtt/mqtt-store.js +538 -0
  13. package/src/core/emitter-manager/terminal/assert-columns.js +98 -0
  14. package/src/core/emitter-manager/terminal/formatters.js +160 -0
  15. package/src/core/emitter-manager/terminal/index.js +341 -0
  16. package/src/core/env-vars.js +245 -0
  17. package/src/core/mqtt-protocol.js +47 -0
  18. package/src/core/partition-manager/index.js +7 -0
  19. package/src/core/partition-manager/init.js +44 -0
  20. package/src/core/partition-manager/introspect.js +15 -0
  21. package/src/core/partition-manager/publish-to.js +6 -0
  22. package/src/core/partition-manager/recompute.js +5 -0
  23. package/src/core/partition-manager/reset.js +5 -0
  24. package/src/core/partition-manager/update.js +264 -0
  25. package/src/core/partition-manager/validate-flow.js +157 -0
  26. package/src/core/semantics/digest.js +128 -0
  27. package/src/core/semantics/index.js +83 -0
  28. package/src/core/semantics/loader.js +571 -0
  29. package/src/core/semantics/schemas/asset-class-schema.js +218 -0
  30. package/src/core/semantics/schemas/column-schema.js +651 -0
  31. package/src/core/semantics/schemas/enum-schema.js +153 -0
  32. package/src/core/semantics/schemas/index.js +51 -0
  33. package/src/core/semantics/warnings.js +141 -0
  34. package/src/core/shutdown-manager/index.js +289 -0
  35. package/src/core/source-manager/csv/index.js +171 -0
  36. package/src/core/source-manager/csv/start.js +456 -0
  37. package/src/core/source-manager/mqtt/client.js +406 -0
  38. package/src/core/source-manager/mqtt/constants.js +147 -0
  39. package/src/core/source-manager/mqtt/dedup.js +244 -0
  40. package/src/core/source-manager/mqtt/index.js +260 -0
  41. package/src/core/source-manager/mqtt/status.js +546 -0
  42. package/src/core/source-manager/test-harness/comparator.js +226 -0
  43. package/src/core/source-manager/test-harness/field-generator.js +85 -0
  44. package/src/core/source-manager/test-harness/fuzz.js +87 -0
  45. package/src/core/source-manager/test-harness/index.js +115 -0
  46. package/src/core/source-manager/test-harness/prng.js +57 -0
  47. package/src/core/source-manager/test-harness/start.js +232 -0
  48. package/src/core/source-manager/test-harness/validate.js +213 -0
  49. package/src/core/storage-manager/questdb/assert-columns.js +120 -0
  50. package/src/core/storage-manager/questdb/ensure-tables.js +187 -0
  51. package/src/core/storage-manager/questdb/index.js +1251 -0
  52. package/src/core/storage-manager/questdb/persist-plan.js +488 -0
  53. package/src/core/storage-manager/questdb/writers.js +142 -0
  54. package/src/core/test-utils/sealed-record.js +80 -0
  55. package/src/core/test-utils/soak-signature.js +149 -0
  56. package/src/core/test-utils/tcp-proxy.js +106 -0
  57. package/src/core/tunable/helpers.js +254 -0
  58. package/src/core/tunable/index.js +87 -0
  59. package/src/core/utils/console-formatter/index.js +207 -0
  60. package/src/core/utils/create-smart-store-as/index.js +98 -0
  61. package/src/core/utils/create-smart-store-as/sanitize-field.js +46 -0
  62. package/src/core/utils/create-smart-store-as/smart-truncate.js +93 -0
  63. package/src/core/utils/flow/index.js +5 -0
  64. package/src/core/utils/flow/make-collision-checker.js +23 -0
  65. package/src/core/utils/flow/to-kebab.js +20 -0
  66. package/src/core/utils/half-life/index.js +111 -0
  67. package/src/core/utils/node/disable.js +11 -0
  68. package/src/core/utils/node/enable.js +11 -0
  69. package/src/core/utils/node/execute-triggers.js +40 -0
  70. package/src/core/utils/node/index.js +10 -0
  71. package/src/core/utils/node/pause.js +11 -0
  72. package/src/core/utils/node/populate-predicate-inputs.js +14 -0
  73. package/src/core/utils/node/publish-nan.js +19 -0
  74. package/src/core/utils/node/reset-predicate-inputs.js +11 -0
  75. package/src/core/utils/node/unpause.js +11 -0
  76. package/src/core/utils/node/validate-spec.js +22 -0
  77. package/src/core/utils/options/resolve-field-keyed.js +115 -0
  78. package/src/core/utils/quantize/index.js +75 -0
  79. package/src/core/utils/stats/formulas.js +67 -0
  80. package/src/core/utils/stats/index.js +3 -0
  81. package/src/core/utils/validate/composer-validators.js +141 -0
  82. package/src/core/utils/validate/helpers.js +206 -0
  83. package/src/core/utils/validate/index.js +12 -0
  84. package/src/core/utils/validate/type-validators.js +538 -0
  85. package/src/core/utils/validate/validate.js +107 -0
  86. package/src/core/utils/validate/validators.js +177 -0
  87. package/src/core/wiring/assert-handle.js +57 -0
  88. package/src/core/wiring/assert-module.js +63 -0
  89. package/src/core/wiring/index.js +5 -0
  90. package/src/core/wiring/node-type-to-module.js +17 -0
  91. package/src/core/wiring/wire-emitters.js +256 -0
  92. package/src/core/wiring/wire-linear-graph.js +25 -0
  93. package/src/core/wiring/wire-node.js +157 -0
  94. package/src/core/wiring/wire-semantics.js +173 -0
  95. package/src/core/wiring/wire-storages.js +344 -0
  96. package/src/flow/check-output-collisions.js +93 -0
  97. package/src/flow/consts.js +9 -0
  98. package/src/flow/deep-clone-spec.js +64 -0
  99. package/src/flow/driver.js +171 -0
  100. package/src/flow/expand-single-input-array.js +76 -0
  101. package/src/flow/expand-template-spec.js +93 -0
  102. package/src/flow/flow-api-schemas.js +300 -0
  103. package/src/flow/flow.js +658 -0
  104. package/src/flow/for-each.js +225 -0
  105. package/src/flow/get-signature-pattern.js +31 -0
  106. package/src/flow/get-target-array.js +46 -0
  107. package/src/flow/inspect.js +67 -0
  108. package/src/flow/load-node-module.js +81 -0
  109. package/src/flow/make-node-method.js +112 -0
  110. package/src/flow/resolve-pick-by-field.js +77 -0
  111. package/src/flow/resolve-tunables.js +76 -0
  112. package/src/flow/run.js +392 -0
  113. package/src/flow/serialize.js +161 -0
  114. package/src/flow/signatures-schema.js +222 -0
  115. package/src/flow/spec-builder.js +65 -0
  116. package/src/flow/validate-triggers.js +250 -0
  117. package/src/flow/validate.js +261 -0
  118. package/src/nodes/accumulate/index.js +21 -0
  119. package/src/nodes/accumulate/init.js +48 -0
  120. package/src/nodes/accumulate/introspect.js +123 -0
  121. package/src/nodes/accumulate/publish-to.js +34 -0
  122. package/src/nodes/accumulate/recompute.js +27 -0
  123. package/src/nodes/accumulate/reset.js +25 -0
  124. package/src/nodes/accumulate/update.js +42 -0
  125. package/src/nodes/appraise/calibrate.js +64 -0
  126. package/src/nodes/appraise/decision.js +66 -0
  127. package/src/nodes/appraise/deviation.js +140 -0
  128. package/src/nodes/appraise/index.js +23 -0
  129. package/src/nodes/appraise/init.js +165 -0
  130. package/src/nodes/appraise/integrate.js +45 -0
  131. package/src/nodes/appraise/introspect.js +352 -0
  132. package/src/nodes/appraise/publish-to.js +93 -0
  133. package/src/nodes/appraise/receptor.js +69 -0
  134. package/src/nodes/appraise/recompute.js +79 -0
  135. package/src/nodes/appraise/reset.js +53 -0
  136. package/src/nodes/appraise/update.js +130 -0
  137. package/src/nodes/archive/csv/index.js +6 -0
  138. package/src/nodes/archive/csv/init.js +36 -0
  139. package/src/nodes/archive/csv/load.js +70 -0
  140. package/src/nodes/archive/csv/reset.js +14 -0
  141. package/src/nodes/archive/csv/simple-parser.js +216 -0
  142. package/src/nodes/archive/csv/test.js +173 -0
  143. package/src/nodes/archive/csv/update.js +63 -0
  144. package/src/nodes/butterworth-filter/index.js +18 -0
  145. package/src/nodes/butterworth-filter/init.js +184 -0
  146. package/src/nodes/butterworth-filter/introspect.js +228 -0
  147. package/src/nodes/butterworth-filter/publish-to.js +21 -0
  148. package/src/nodes/butterworth-filter/recompute.js +13 -0
  149. package/src/nodes/butterworth-filter/reset.js +30 -0
  150. package/src/nodes/butterworth-filter/update.js +38 -0
  151. package/src/nodes/categorize/index.js +19 -0
  152. package/src/nodes/categorize/init.js +58 -0
  153. package/src/nodes/categorize/introspect.js +177 -0
  154. package/src/nodes/categorize/publish-to.js +29 -0
  155. package/src/nodes/categorize/recompute.js +14 -0
  156. package/src/nodes/categorize/reset.js +16 -0
  157. package/src/nodes/categorize/update.js +53 -0
  158. package/src/nodes/controller/index.js +9 -0
  159. package/src/nodes/controller/init.js +55 -0
  160. package/src/nodes/controller/introspect.js +91 -0
  161. package/src/nodes/controller/publish-to.js +8 -0
  162. package/src/nodes/controller/recompute.js +9 -0
  163. package/src/nodes/controller/reset.js +17 -0
  164. package/src/nodes/controller/update.js +63 -0
  165. package/src/nodes/diff/index.js +19 -0
  166. package/src/nodes/diff/init.js +37 -0
  167. package/src/nodes/diff/introspect.js +132 -0
  168. package/src/nodes/diff/publish-to.js +21 -0
  169. package/src/nodes/diff/recompute.js +12 -0
  170. package/src/nodes/diff/reset.js +12 -0
  171. package/src/nodes/diff/update.js +37 -0
  172. package/src/nodes/digest-moments/formulas.js +12 -0
  173. package/src/nodes/digest-moments/index.js +14 -0
  174. package/src/nodes/digest-moments/init.js +67 -0
  175. package/src/nodes/digest-moments/introspect.js +154 -0
  176. package/src/nodes/digest-moments/publish-to.js +34 -0
  177. package/src/nodes/digest-moments/recompute.js +14 -0
  178. package/src/nodes/digest-moments/reset.js +14 -0
  179. package/src/nodes/digest-moments/update.js +120 -0
  180. package/src/nodes/dwell-time-tracker/index.js +14 -0
  181. package/src/nodes/dwell-time-tracker/init.js +59 -0
  182. package/src/nodes/dwell-time-tracker/introspect.js +137 -0
  183. package/src/nodes/dwell-time-tracker/publish-to.js +22 -0
  184. package/src/nodes/dwell-time-tracker/recompute.js +8 -0
  185. package/src/nodes/dwell-time-tracker/reset.js +23 -0
  186. package/src/nodes/dwell-time-tracker/update.js +103 -0
  187. package/src/nodes/emit-if/helpers.js +94 -0
  188. package/src/nodes/emit-if/index.js +16 -0
  189. package/src/nodes/emit-if/init.js +67 -0
  190. package/src/nodes/emit-if/introspect.js +102 -0
  191. package/src/nodes/emit-if/publish-to.js +13 -0
  192. package/src/nodes/emit-if/recompute.js +12 -0
  193. package/src/nodes/emit-if/reset.js +27 -0
  194. package/src/nodes/emit-if/update.js +93 -0
  195. package/src/nodes/es-correlation/index.js +19 -0
  196. package/src/nodes/es-correlation/init.js +73 -0
  197. package/src/nodes/es-correlation/introspect.js +176 -0
  198. package/src/nodes/es-correlation/publish-to.js +32 -0
  199. package/src/nodes/es-correlation/recompute.js +56 -0
  200. package/src/nodes/es-correlation/reset.js +27 -0
  201. package/src/nodes/es-correlation/update.js +110 -0
  202. package/src/nodes/es-mean/index.js +20 -0
  203. package/src/nodes/es-mean/init.js +58 -0
  204. package/src/nodes/es-mean/introspect.js +129 -0
  205. package/src/nodes/es-mean/publish-to.js +19 -0
  206. package/src/nodes/es-mean/recompute.js +16 -0
  207. package/src/nodes/es-mean/reset.js +24 -0
  208. package/src/nodes/es-mean/update.js +69 -0
  209. package/src/nodes/es-pairwise-correlation/index.js +19 -0
  210. package/src/nodes/es-pairwise-correlation/init.js +98 -0
  211. package/src/nodes/es-pairwise-correlation/introspect.js +187 -0
  212. package/src/nodes/es-pairwise-correlation/publish-to.js +31 -0
  213. package/src/nodes/es-pairwise-correlation/recompute.js +69 -0
  214. package/src/nodes/es-pairwise-correlation/reset.js +29 -0
  215. package/src/nodes/es-pairwise-correlation/update.js +89 -0
  216. package/src/nodes/es-stats/index.js +21 -0
  217. package/src/nodes/es-stats/init.js +105 -0
  218. package/src/nodes/es-stats/introspect.js +157 -0
  219. package/src/nodes/es-stats/publish-to.js +42 -0
  220. package/src/nodes/es-stats/recompute.js +74 -0
  221. package/src/nodes/es-stats/reset.js +49 -0
  222. package/src/nodes/es-stats/update-envelope.js +44 -0
  223. package/src/nodes/es-stats/update-welford.js +64 -0
  224. package/src/nodes/es-stats/update.js +59 -0
  225. package/src/nodes/index.js +83 -0
  226. package/src/nodes/invert-flag/index.js +20 -0
  227. package/src/nodes/invert-flag/init.js +36 -0
  228. package/src/nodes/invert-flag/introspect.js +115 -0
  229. package/src/nodes/invert-flag/publish-to.js +23 -0
  230. package/src/nodes/invert-flag/recompute.js +12 -0
  231. package/src/nodes/invert-flag/reset.js +12 -0
  232. package/src/nodes/invert-flag/update.js +32 -0
  233. package/src/nodes/kalman1d/index.js +20 -0
  234. package/src/nodes/kalman1d/init.js +103 -0
  235. package/src/nodes/kalman1d/introspect.js +250 -0
  236. package/src/nodes/kalman1d/publish-to.js +34 -0
  237. package/src/nodes/kalman1d/recompute.js +23 -0
  238. package/src/nodes/kalman1d/reset.js +29 -0
  239. package/src/nodes/kalman1d/update.js +144 -0
  240. package/src/nodes/kernel/index.js +18 -0
  241. package/src/nodes/kernel/init.js +66 -0
  242. package/src/nodes/kernel/introspect.js +163 -0
  243. package/src/nodes/kernel/presets.js +74 -0
  244. package/src/nodes/kernel/publish-to.js +25 -0
  245. package/src/nodes/kernel/recompute.js +14 -0
  246. package/src/nodes/kernel/reset.js +20 -0
  247. package/src/nodes/kernel/update.js +69 -0
  248. package/src/nodes/lag/index.js +21 -0
  249. package/src/nodes/lag/init.js +87 -0
  250. package/src/nodes/lag/introspect.js +202 -0
  251. package/src/nodes/lag/publish-to.js +64 -0
  252. package/src/nodes/lag/recompute.js +33 -0
  253. package/src/nodes/lag/reset.js +49 -0
  254. package/src/nodes/lag/update.js +133 -0
  255. package/src/nodes/median3/index.js +19 -0
  256. package/src/nodes/median3/init.js +40 -0
  257. package/src/nodes/median3/introspect.js +116 -0
  258. package/src/nodes/median3/publish-to.js +21 -0
  259. package/src/nodes/median3/recompute.js +12 -0
  260. package/src/nodes/median3/reset.js +15 -0
  261. package/src/nodes/median3/update.js +58 -0
  262. package/src/nodes/moments-digest/copy-from.js +12 -0
  263. package/src/nodes/moments-digest/flush.js +17 -0
  264. package/src/nodes/moments-digest/index.js +132 -0
  265. package/src/nodes/moments-digest/init.js +84 -0
  266. package/src/nodes/moments-digest/introspect.js +122 -0
  267. package/src/nodes/moments-digest/prelude.js +28 -0
  268. package/src/nodes/moments-digest/publish-to.js +31 -0
  269. package/src/nodes/moments-digest/recompute.js +9 -0
  270. package/src/nodes/moments-digest/reset.js +20 -0
  271. package/src/nodes/moments-digest/update.js +182 -0
  272. package/src/nodes/node-names.js +67 -0
  273. package/src/nodes/page-hinkley/index.js +20 -0
  274. package/src/nodes/page-hinkley/init.js +66 -0
  275. package/src/nodes/page-hinkley/introspect.js +193 -0
  276. package/src/nodes/page-hinkley/publish-to.js +28 -0
  277. package/src/nodes/page-hinkley/recompute.js +14 -0
  278. package/src/nodes/page-hinkley/reset.js +23 -0
  279. package/src/nodes/page-hinkley/update.js +105 -0
  280. package/src/nodes/pass-if/index.js +21 -0
  281. package/src/nodes/pass-if/init.js +37 -0
  282. package/src/nodes/pass-if/introspect.js +93 -0
  283. package/src/nodes/pass-if/publish-to.js +15 -0
  284. package/src/nodes/pass-if/recompute.js +15 -0
  285. package/src/nodes/pass-if/reset.js +18 -0
  286. package/src/nodes/pass-if/update.js +37 -0
  287. package/src/nodes/persist-if/helpers.js +111 -0
  288. package/src/nodes/persist-if/index.js +9 -0
  289. package/src/nodes/persist-if/init.js +98 -0
  290. package/src/nodes/persist-if/introspect.js +106 -0
  291. package/src/nodes/persist-if/publish-to.js +21 -0
  292. package/src/nodes/persist-if/recompute.js +20 -0
  293. package/src/nodes/persist-if/reset.js +35 -0
  294. package/src/nodes/persist-if/update.js +112 -0
  295. package/src/nodes/persistence-check/index.js +14 -0
  296. package/src/nodes/persistence-check/init.js +38 -0
  297. package/src/nodes/persistence-check/introspect.js +144 -0
  298. package/src/nodes/persistence-check/publish-to.js +11 -0
  299. package/src/nodes/persistence-check/recompute.js +7 -0
  300. package/src/nodes/persistence-check/reset.js +12 -0
  301. package/src/nodes/persistence-check/update.js +57 -0
  302. package/src/nodes/process-index/index.js +15 -0
  303. package/src/nodes/process-index/init.js +48 -0
  304. package/src/nodes/process-index/introspect.js +182 -0
  305. package/src/nodes/process-index/publish-to.js +29 -0
  306. package/src/nodes/process-index/recompute.js +8 -0
  307. package/src/nodes/process-index/reset.js +8 -0
  308. package/src/nodes/process-index/update.js +75 -0
  309. package/src/nodes/ratio/index.js +14 -0
  310. package/src/nodes/ratio/init.js +37 -0
  311. package/src/nodes/ratio/introspect.js +155 -0
  312. package/src/nodes/ratio/publish-to.js +15 -0
  313. package/src/nodes/ratio/recompute.js +7 -0
  314. package/src/nodes/ratio/reset.js +7 -0
  315. package/src/nodes/ratio/update.js +47 -0
  316. package/src/nodes/sanitize/index.js +82 -0
  317. package/src/nodes/sanitize/init.js +82 -0
  318. package/src/nodes/sanitize/introspect.js +194 -0
  319. package/src/nodes/sanitize/publish-to.js +15 -0
  320. package/src/nodes/sanitize/recompute.js +8 -0
  321. package/src/nodes/sanitize/reset.js +14 -0
  322. package/src/nodes/sanitize/update.js +102 -0
  323. package/src/nodes/spike-guard/index.js +14 -0
  324. package/src/nodes/spike-guard/init.js +47 -0
  325. package/src/nodes/spike-guard/introspect.js +130 -0
  326. package/src/nodes/spike-guard/publish-to.js +35 -0
  327. package/src/nodes/spike-guard/recompute.js +14 -0
  328. package/src/nodes/spike-guard/reset.js +19 -0
  329. package/src/nodes/spike-guard/update.js +89 -0
  330. package/src/nodes/sse-emitter/index.js +7 -0
  331. package/src/nodes/sse-emitter/init.js +105 -0
  332. package/src/nodes/sse-emitter/introspect.js +134 -0
  333. package/src/nodes/sse-emitter/publish-to.js +6 -0
  334. package/src/nodes/sse-emitter/recompute.js +16 -0
  335. package/src/nodes/sse-emitter/reset.js +19 -0
  336. package/src/nodes/sse-emitter/update.js +69 -0
  337. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +36 -0
  338. package/src/nodes/sse-emitter/utils/build-message.js +31 -0
  339. package/src/nodes/sse-emitter/utils/connection-manager.js +45 -0
  340. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +20 -0
  341. package/src/nodes/sse-emitter/utils/health-monitor.js +38 -0
  342. package/src/nodes/sse-emitter/utils/qos-handler.js +41 -0
  343. package/src/nodes/state-change-detector/helpers.js +35 -0
  344. package/src/nodes/state-change-detector/index.js +12 -0
  345. package/src/nodes/state-change-detector/init.js +56 -0
  346. package/src/nodes/state-change-detector/introspect.js +146 -0
  347. package/src/nodes/state-change-detector/publish-to.js +20 -0
  348. package/src/nodes/state-change-detector/recompute.js +6 -0
  349. package/src/nodes/state-change-detector/reset.js +20 -0
  350. package/src/nodes/state-change-detector/update.js +97 -0
  351. package/src/nodes/sw-stats/index.js +14 -0
  352. package/src/nodes/sw-stats/init.js +51 -0
  353. package/src/nodes/sw-stats/introspect.js +131 -0
  354. package/src/nodes/sw-stats/publish-to.js +45 -0
  355. package/src/nodes/sw-stats/recompute.js +33 -0
  356. package/src/nodes/sw-stats/reset.js +15 -0
  357. package/src/nodes/sw-stats/update.js +39 -0
  358. package/src/nodes/swing-watch/index.js +17 -0
  359. package/src/nodes/swing-watch/init.js +144 -0
  360. package/src/nodes/swing-watch/introspect.js +188 -0
  361. package/src/nodes/swing-watch/publish-to.js +53 -0
  362. package/src/nodes/swing-watch/recompute.js +22 -0
  363. package/src/nodes/swing-watch/reset.js +55 -0
  364. package/src/nodes/swing-watch/update.js +767 -0
  365. package/src/nodes/tally/index.js +15 -0
  366. package/src/nodes/tally/init.js +32 -0
  367. package/src/nodes/tally/introspect.js +116 -0
  368. package/src/nodes/tally/publish-to.js +28 -0
  369. package/src/nodes/tally/recompute.js +10 -0
  370. package/src/nodes/tally/reset.js +10 -0
  371. package/src/nodes/tally/update.js +44 -0
  372. package/src/nodes/threshold/index.js +14 -0
  373. package/src/nodes/threshold/init.js +121 -0
  374. package/src/nodes/threshold/introspect.js +192 -0
  375. package/src/nodes/threshold/publish-to.js +11 -0
  376. package/src/nodes/threshold/recompute.js +8 -0
  377. package/src/nodes/threshold/reset.js +14 -0
  378. package/src/nodes/threshold/update.js +54 -0
  379. package/src/nodes/transform/helpers.js +39 -0
  380. package/src/nodes/transform/index.js +15 -0
  381. package/src/nodes/transform/init.js +35 -0
  382. package/src/nodes/transform/introspect.js +118 -0
  383. package/src/nodes/transform/publish-to.js +23 -0
  384. package/src/nodes/transform/recompute.js +12 -0
  385. package/src/nodes/transform/reset.js +11 -0
  386. package/src/nodes/transform/update.js +32 -0
  387. package/src/nodes/trend/compute-confidence.js +112 -0
  388. package/src/nodes/trend/index.js +14 -0
  389. package/src/nodes/trend/init.js +81 -0
  390. package/src/nodes/trend/introspect.js +151 -0
  391. package/src/nodes/trend/publish-to.js +22 -0
  392. package/src/nodes/trend/recompute.js +15 -0
  393. package/src/nodes/trend/reset.js +26 -0
  394. package/src/nodes/trend/update.js +93 -0
  395. package/src/nodes/tw-stats/copy-from.js +16 -0
  396. package/src/nodes/tw-stats/flush.js +12 -0
  397. package/src/nodes/tw-stats/index.js +41 -0
  398. package/src/nodes/tw-stats/init.js +92 -0
  399. package/src/nodes/tw-stats/introspect.js +171 -0
  400. package/src/nodes/tw-stats/publish-to.js +108 -0
  401. package/src/nodes/tw-stats/recompute.js +9 -0
  402. package/src/nodes/tw-stats/reset.js +16 -0
  403. package/src/nodes/tw-stats/update.js +113 -0
  404. package/src/nodes/unbalance/index.js +15 -0
  405. package/src/nodes/unbalance/init.js +59 -0
  406. package/src/nodes/unbalance/introspect.js +167 -0
  407. package/src/nodes/unbalance/publish-to.js +52 -0
  408. package/src/nodes/unbalance/recompute.js +11 -0
  409. package/src/nodes/unbalance/reset.js +10 -0
  410. package/src/nodes/unbalance/update.js +83 -0
  411. package/src/nodes/vector-distance/index.js +14 -0
  412. package/src/nodes/vector-distance/init.js +54 -0
  413. package/src/nodes/vector-distance/introspect.js +149 -0
  414. package/src/nodes/vector-distance/publish-to.js +35 -0
  415. package/src/nodes/vector-distance/recompute.js +11 -0
  416. package/src/nodes/vector-distance/reset.js +20 -0
  417. package/src/nodes/vector-distance/update.js +139 -0
  418. package/src/nodes/winnow/index.js +12 -0
  419. package/src/nodes/winnow/init.js +82 -0
  420. package/src/nodes/winnow/introspect.js +253 -0
  421. package/src/nodes/winnow/publish-to.js +51 -0
  422. package/src/nodes/winnow/recompute.js +14 -0
  423. package/src/nodes/winnow/reset.js +34 -0
  424. package/src/nodes/winnow/update.js +203 -0
  425. package/src/tools/training/features.js +227 -0
  426. package/src/tools/training/index.js +21 -0
  427. package/src/tools/training/logistic.js +238 -0
  428. package/src/tools/training/metrics.js +126 -0
  429. package/src/tools/training/scale.js +239 -0
  430. package/src/tools/training/validate.js +103 -0
  431. package/src/windowing/count-sliding/create.js +16 -0
  432. package/src/windowing/count-sliding/get-reverse-chronological-index.js +19 -0
  433. package/src/windowing/count-sliding/index.js +6 -0
  434. package/src/windowing/count-sliding/is-not-full.js +5 -0
  435. package/src/windowing/count-sliding/push.js +15 -0
  436. package/src/windowing/count-sliding/reset.js +11 -0
  437. package/src/windowing/count-sliding/size.js +5 -0
  438. package/src/windowing/time-sliding/create.js +25 -0
  439. package/src/windowing/time-sliding/push.js +42 -0
  440. package/CODE_OF_CONDUCT.md +0 -46
  441. package/CONTRIBUTING.md +0 -132
  442. package/docs/.nojekyll +0 -0
  443. package/docs/CONTRIBUTING.md +0 -134
  444. package/docs/README.md +0 -132
  445. package/docs/_coverpage.md +0 -26
  446. package/docs/_media/bg.png +0 -0
  447. package/docs/_media/wink-logo.png +0 -0
  448. package/docs/_navbar.md +0 -14
  449. package/docs/_sidebar.md +0 -2
  450. package/docs/index.html +0 -44
  451. package/eslint.config.js +0 -283
@@ -0,0 +1,227 @@
1
+ // src/tools/training/features.js
2
+
3
+ /**
4
+ * @fileoverview Declarative feature extraction for physics-informed models.
5
+ *
6
+ * Three schema builders describe how to extract features from raw records:
7
+ * group() — aggregate across symmetric channels (enforces physics symmetry)
8
+ * scalar() — extract a single numeric value
9
+ * counter() — count entries satisfying a predicate
10
+ *
11
+ * buildExtractor() compiles a schema into { extract, names, width }.
12
+ * The compiled extract() produces a fixed-width numeric vector from each
13
+ * record, suitable for logistic regression or any classifier.
14
+ *
15
+ * Design principle: group() is the physics constraint — it aggregates
16
+ * per-channel values into layer-level statistics, making the model
17
+ * structurally unable to learn channel-specific preferences.
18
+ */
19
+
20
+ const GROUP = 'group';
21
+ const SCALAR = 'scalar';
22
+ const COUNTER = 'counter';
23
+
24
+ const AVAILABLE_STATS = [ 'max', 'sum', 'countAbove' ];
25
+
26
+ /**
27
+ * Schema builder: aggregate across symmetric channels.
28
+ *
29
+ * The group constraint enforces physics symmetry — the model sees
30
+ * layer-level aggregates, not individual channel values. Each stat
31
+ * becomes a separate feature named `{name}_{stat}`.
32
+ *
33
+ * @param {string} name — base name for generated features (e.g. 'mass').
34
+ * @param {string[]} ids — channel identifiers (e.g. ['T1', 'T3', 'T4']).
35
+ * @param {Function} accessor — ( record, id ) → number. Extracts a value per channel.
36
+ * @param {object} [options] — { stats: string[], countAboveThreshold: number }.
37
+ * @returns {object} column descriptor.
38
+ */
39
+ const group = function ( name, ids, accessor, options ) {
40
+ if ( typeof name !== 'string' || name.length === 0 ) {
41
+ throw new Error( 'features.group: name must be a non-empty string' );
42
+ }
43
+ if ( !Array.isArray( ids ) || ids.length === 0 ) {
44
+ throw new Error( 'features.group: ids must be a non-empty array' );
45
+ }
46
+ if ( typeof accessor !== 'function' ) {
47
+ throw new Error( 'features.group: accessor must be a function' );
48
+ }
49
+
50
+ const stats = ( options && Array.isArray( options.stats ) ) ?
51
+ options.stats :
52
+ [ 'max', 'sum', 'countAbove' ];
53
+
54
+ for ( let i = 0; i < stats.length; i += 1 ) {
55
+ if ( AVAILABLE_STATS.indexOf( stats[ i ] ) === -1 ) {
56
+ throw new Error(
57
+ 'features.group: unknown stat "' + stats[ i ] +
58
+ '". Available: ' + AVAILABLE_STATS.join( ', ' )
59
+ );
60
+ }
61
+ }
62
+
63
+ const countAboveThreshold = ( options && options.countAboveThreshold !== undefined ) ?
64
+ options.countAboveThreshold :
65
+ 1.5;
66
+
67
+ if ( typeof countAboveThreshold !== 'number' ) {
68
+ throw new Error( 'features.group: countAboveThreshold must be a number' );
69
+ }
70
+
71
+ return {
72
+ type: GROUP,
73
+ name: name,
74
+ ids: ids,
75
+ accessor: accessor,
76
+ stats: stats,
77
+ countAboveThreshold: countAboveThreshold
78
+ };
79
+ };
80
+
81
+ /**
82
+ * Schema builder: extract a single numeric value.
83
+ *
84
+ * Use for features that are already aggregate or unique (e.g. T6 coupling
85
+ * z-score, cross-tank Mahalanobis distance).
86
+ *
87
+ * @param {string} name — feature name.
88
+ * @param {Function} accessor — ( record ) → number.
89
+ * @returns {object} column descriptor.
90
+ */
91
+ const scalar = function ( name, accessor ) {
92
+ if ( typeof name !== 'string' || name.length === 0 ) {
93
+ throw new Error( 'features.scalar: name must be a non-empty string' );
94
+ }
95
+ if ( typeof accessor !== 'function' ) {
96
+ throw new Error( 'features.scalar: accessor must be a function' );
97
+ }
98
+ return { type: SCALAR, name: name, accessor: accessor };
99
+ };
100
+
101
+ /**
102
+ * Schema builder: count entries satisfying a predicate.
103
+ *
104
+ * Unlike group(), the set of items is dynamic — determined at extraction
105
+ * time from the source object's keys. Use for violation counts where the
106
+ * item set may vary per record.
107
+ *
108
+ * @param {string} name — feature name.
109
+ * @param {Function} sourceAccessor — ( record ) → object. Returns the source object.
110
+ * @param {Function} predicate — ( entry ) → boolean. Tests each value.
111
+ * @returns {object} column descriptor.
112
+ */
113
+ const counter = function ( name, sourceAccessor, predicate ) {
114
+ if ( typeof name !== 'string' || name.length === 0 ) {
115
+ throw new Error( 'features.counter: name must be a non-empty string' );
116
+ }
117
+ if ( typeof sourceAccessor !== 'function' ) {
118
+ throw new Error( 'features.counter: sourceAccessor must be a function' );
119
+ }
120
+ if ( typeof predicate !== 'function' ) {
121
+ throw new Error( 'features.counter: predicate must be a function' );
122
+ }
123
+ return { type: COUNTER, name: name, sourceAccessor: sourceAccessor, predicate: predicate };
124
+ };
125
+
126
+ /**
127
+ * Compile a feature schema into an extractor.
128
+ *
129
+ * Validates the schema for structural consistency (unique names, known
130
+ * column types) and returns a compiled { extract, names, width } object.
131
+ *
132
+ * @param {object[]} schema — array of column descriptors from group/scalar/counter.
133
+ * @returns {{ extract: Function, names: string[], width: number }}
134
+ */
135
+ const buildExtractor = function ( schema ) {
136
+ if ( !Array.isArray( schema ) || schema.length === 0 ) {
137
+ throw new Error( 'features.buildExtractor: schema must be a non-empty array' );
138
+ }
139
+
140
+ // Build feature names from schema
141
+ const names = [];
142
+ for ( let i = 0; i < schema.length; i += 1 ) {
143
+ const col = schema[ i ];
144
+ if ( col.type === GROUP ) {
145
+ for ( let s = 0; s < col.stats.length; s += 1 ) {
146
+ names.push( col.name + '_' + col.stats[ s ] );
147
+ }
148
+ } else if ( col.type === SCALAR || col.type === COUNTER ) {
149
+ names.push( col.name );
150
+ } else {
151
+ throw new Error(
152
+ 'features.buildExtractor: unknown column type "' +
153
+ String( col.type ) + '" at index ' + i
154
+ );
155
+ }
156
+ }
157
+
158
+ // Validate unique names
159
+ const seen = Object.create( null );
160
+ for ( let i = 0; i < names.length; i += 1 ) {
161
+ if ( seen[ names[ i ] ] ) {
162
+ throw new Error(
163
+ 'features.buildExtractor: duplicate feature name "' + names[ i ] + '"'
164
+ );
165
+ }
166
+ seen[ names[ i ] ] = true;
167
+ }
168
+
169
+ const width = names.length;
170
+
171
+ /**
172
+ * Extract a fixed-width feature vector from a record.
173
+ * @param {object} record — raw input record.
174
+ * @returns {number[]} feature vector of length `width`.
175
+ */
176
+ const extract = function ( record ) {
177
+ const v = new Array( width );
178
+ let idx = 0;
179
+
180
+ for ( let i = 0; i < schema.length; i += 1 ) {
181
+ const col = schema[ i ];
182
+
183
+ if ( col.type === GROUP ) {
184
+ let max = 0;
185
+ let sum = 0;
186
+ let countAbove = 0;
187
+
188
+ for ( let j = 0; j < col.ids.length; j += 1 ) {
189
+ const val = col.accessor( record, col.ids[ j ] );
190
+ if ( val > max ) max = val;
191
+ sum += val;
192
+ if ( val > col.countAboveThreshold ) countAbove += 1;
193
+ }
194
+
195
+ for ( let s = 0; s < col.stats.length; s += 1 ) {
196
+ const stat = col.stats[ s ];
197
+ if ( stat === 'max' ) {
198
+ v[ idx ] = max;
199
+ } else if ( stat === 'sum' ) {
200
+ v[ idx ] = sum;
201
+ } else {
202
+ v[ idx ] = countAbove;
203
+ }
204
+ idx += 1;
205
+ }
206
+ } else if ( col.type === SCALAR ) {
207
+ v[ idx ] = col.accessor( record );
208
+ idx += 1;
209
+ } else {
210
+ const source = col.sourceAccessor( record );
211
+ const keys = Object.keys( source );
212
+ let count = 0;
213
+ for ( let k = 0; k < keys.length; k += 1 ) {
214
+ if ( col.predicate( source[ keys[ k ] ] ) ) count += 1;
215
+ }
216
+ v[ idx ] = count;
217
+ idx += 1;
218
+ }
219
+ }
220
+
221
+ return v;
222
+ };
223
+
224
+ return { extract: extract, names: names, width: width };
225
+ };
226
+
227
+ export { group, scalar, counter, buildExtractor };
@@ -0,0 +1,21 @@
1
+ // src/tools/training/index.js
2
+
3
+ /**
4
+ * @fileoverview Public API for the training utility.
5
+ *
6
+ * Re-exports scaling, logistic regression, and evaluation metrics
7
+ * as a flat namespace. Intended for offline model training workflows —
8
+ * the learned coefficients feed into flow configuration (e.g. the future
9
+ * `infer` node).
10
+ */
11
+
12
+ export {
13
+ computeStandardParams,
14
+ computeMinMaxParams,
15
+ computeRobustParams,
16
+ scale,
17
+ standardize
18
+ } from './scale.js';
19
+
20
+ export { sigmoid, train, predict, classify } from './logistic.js';
21
+ export { confusionMatrix, classificationMetrics, sweepThresholds } from './metrics.js';
@@ -0,0 +1,238 @@
1
+ // src/tools/training/logistic.js
2
+
3
+ /**
4
+ * @fileoverview Logistic regression via batch gradient descent with L2
5
+ * regularization and class weighting.
6
+ *
7
+ * Implements the standard binary logistic model (Bishop, 2006, PRML §4.3):
8
+ * P(y=1 | x) = σ( wᵀx + b )
9
+ *
10
+ * Training minimises the weighted cross-entropy loss with optional L2
11
+ * penalty (ridge). The data term is averaged by 1/n while the L2 penalty
12
+ * is not — this makes lambda dataset-size-invariant: the same lambda
13
+ * produces the same coefficients regardless of n (when data is replicated).
14
+ * Class weights automatically balance imbalanced datasets by making the
15
+ * effective sample count equal across classes.
16
+ *
17
+ * Convergence uses relative loss change: |Δloss| / max(1, |prevLoss|),
18
+ * with a minimum iteration guard to avoid premature exit.
19
+ *
20
+ * Hot-path arrays (pHat, grad) are pre-allocated once — zero allocations
21
+ * inside the training loop.
22
+ *
23
+ * Reference:
24
+ * Bishop, C. M. (2006). Pattern Recognition and Machine Learning.
25
+ * Springer. Chapter 4.3 — Probabilistic Discriminative Models.
26
+ */
27
+
28
+ import { validateMatrix, validateBinaryLabels } from './validate.js';
29
+
30
+ // ── Sigmoid ───────────────────────────────────────────────────────
31
+
32
+ const SIGMOID_UPPER = 1 - 1e-15;
33
+ const SIGMOID_LOWER = 1e-15;
34
+
35
+ /**
36
+ * Logistic sigmoid, clamped to [-500, 500] to prevent exp overflow.
37
+ * Never returns exact 0 or 1 — prevents log(p) from producing -Infinity.
38
+ *
39
+ * @param {number} z — Linear predictor.
40
+ * @returns {number} σ(z) ∈ (1e-15, 1 - 1e-15).
41
+ */
42
+ const sigmoid = function ( z ) {
43
+ if ( z > 500 ) return SIGMOID_UPPER;
44
+ if ( z < -500 ) return SIGMOID_LOWER;
45
+ return 1 / ( 1 + Math.exp( -z ) );
46
+ }; // sigmoid()
47
+
48
+ // ── Defaults ──────────────────────────────────────────────────────
49
+
50
+ const DEFAULTS = Object.freeze( {
51
+ learningRate: 0.1,
52
+ maxIter: 1000,
53
+ minIter: 5,
54
+ lambda: 0.01,
55
+ classWeight: null,
56
+ convergenceTol: 1e-6
57
+ } );
58
+
59
+ // ── Train ─────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Train a binary logistic regression model.
63
+ *
64
+ * @param {number[][]} X — Feature matrix (n × p), already scaled.
65
+ * @param {number[]} y — Label vector, each element exactly 0 or 1.
66
+ * @param {object} [options] — Training hyper-parameters.
67
+ * @param {number} [options.learningRate=0.1]
68
+ * @param {number} [options.maxIter=1000]
69
+ * @param {number} [options.minIter=5] — Minimum iterations before convergence check.
70
+ * @param {number} [options.lambda=0.01] — L2 regularization strength.
71
+ * @param {string|null} [options.classWeight=null] — 'auto' or null.
72
+ * @param {number} [options.convergenceTol=1e-6]
73
+ * @returns {{ coefficients: Float64Array, intercept: number, loss: number, iterations: number }}
74
+ */
75
+ const train = function ( X, y, options ) {
76
+ // ── Validate ──────────────────────────────────────────────────
77
+ const { n, p } = validateMatrix( X, 'train' );
78
+ validateBinaryLabels( y, n, 'train' );
79
+
80
+ // ── Options ───────────────────────────────────────────────────
81
+ const opts = Object.assign( Object.create( null ), DEFAULTS, options );
82
+ const alpha = opts.learningRate;
83
+ const maxIter = opts.maxIter;
84
+ const minIter = opts.minIter;
85
+ const lambda = opts.lambda;
86
+ const tol = opts.convergenceTol;
87
+
88
+ // ── Class weights ─────────────────────────────────────────────
89
+ // Auto-compute: w_k = n / (2 × n_k) — equalises effective counts.
90
+ const w = new Float64Array( 2 ); // w[0] = weight for class 0, w[1] for class 1
91
+ if ( opts.classWeight === 'auto' ) {
92
+ let n1 = 0;
93
+ for ( let i = 0; i < n; i += 1 ) n1 += y[ i ];
94
+ const n0 = n - n1;
95
+ if ( n0 === 0 || n1 === 0 ) {
96
+ throw new Error( 'train: y must contain both classes (0 and 1).' );
97
+ }
98
+ w[ 0 ] = n / ( 2 * n0 );
99
+ w[ 1 ] = n / ( 2 * n1 );
100
+ } else {
101
+ w[ 0 ] = 1;
102
+ w[ 1 ] = 1;
103
+ }
104
+
105
+ // ── Initialise ────────────────────────────────────────────────
106
+ const coefficients = new Float64Array( p ); // zeros
107
+ let intercept = 0;
108
+
109
+ // Pre-allocated working arrays — zero allocations in the loop.
110
+ const pHat = new Float64Array( n );
111
+ const grad = new Float64Array( p );
112
+
113
+ let prevLoss = Infinity;
114
+ let loss = 0;
115
+ let iterations = 0;
116
+
117
+ // ── Gradient descent loop ─────────────────────────────────────
118
+ for ( let iter = 0; iter < maxIter; iter += 1 ) {
119
+ iterations = iter + 1;
120
+ // ── Forward pass: z_i = wᵀx_i + b → pHat_i = σ(z_i) ─────
121
+ for ( let i = 0; i < n; i += 1 ) {
122
+ const row = X[ i ];
123
+ let z = intercept;
124
+ for ( let j = 0; j < p; j += 1 ) {
125
+ z += coefficients[ j ] * row[ j ];
126
+ }
127
+ pHat[ i ] = sigmoid( z );
128
+ }
129
+
130
+ // ── Loss: weighted cross-entropy + L2 penalty ─────────────
131
+ loss = 0;
132
+ for ( let i = 0; i < n; i += 1 ) {
133
+ const yi = y[ i ];
134
+ const pi = pHat[ i ];
135
+ const wi = w[ yi ];
136
+ // Sigmoid never returns exact 0 or 1, so log is safe.
137
+ loss -= wi * ( ( yi * Math.log( pi ) ) + ( ( 1 - yi ) * Math.log( 1 - pi ) ) );
138
+ }
139
+ loss /= n;
140
+ // L2 penalty on coefficients (not intercept).
141
+ let l2 = 0;
142
+ for ( let j = 0; j < p; j += 1 ) {
143
+ l2 += coefficients[ j ] * coefficients[ j ];
144
+ }
145
+ loss += ( lambda / 2 ) * l2;
146
+
147
+ // ── Convergence check (relative, with minIter guard) ──────
148
+ if ( iter >= minIter ) {
149
+ const relChange = Math.abs( prevLoss - loss ) / Math.max( 1, Math.abs( prevLoss ) );
150
+ if ( relChange < tol ) break;
151
+ }
152
+ prevLoss = loss;
153
+
154
+ // ── Gradient ──────────────────────────────────────────────
155
+ // Zero gradient accumulators.
156
+ for ( let j = 0; j < p; j += 1 ) grad[ j ] = 0;
157
+ let g0 = 0;
158
+
159
+ for ( let i = 0; i < n; i += 1 ) {
160
+ const row = X[ i ];
161
+ const err = pHat[ i ] - y[ i ];
162
+ const wi = w[ y[ i ] ];
163
+ const we = wi * err;
164
+ for ( let j = 0; j < p; j += 1 ) {
165
+ grad[ j ] += we * row[ j ];
166
+ }
167
+ g0 += we;
168
+ }
169
+ // Normalise and add L2 penalty to coefficient gradient.
170
+ for ( let j = 0; j < p; j += 1 ) {
171
+ grad[ j ] = ( grad[ j ] / n ) + ( lambda * coefficients[ j ] );
172
+ }
173
+ g0 /= n; // intercept: no regularization
174
+
175
+ // ── Update ────────────────────────────────────────────────
176
+ for ( let j = 0; j < p; j += 1 ) {
177
+ coefficients[ j ] -= alpha * grad[ j ];
178
+ }
179
+ intercept -= alpha * g0;
180
+ }
181
+
182
+ return {
183
+ coefficients: coefficients,
184
+ intercept: intercept,
185
+ loss: loss,
186
+ iterations: iterations
187
+ };
188
+ }; // train()
189
+
190
+ // ── Predict ───────────────────────────────────────────────────────
191
+
192
+ /**
193
+ * Compute predicted probabilities for a feature matrix.
194
+ *
195
+ * @param {number[][]} X — Feature matrix (n × p).
196
+ * @param {Float64Array} coefficients — Model weights.
197
+ * @param {number} intercept — Model bias.
198
+ * @returns {Float64Array} Probabilities, one per row.
199
+ */
200
+ const predict = function ( X, coefficients, intercept ) {
201
+ const n = X.length;
202
+ const p = coefficients.length;
203
+ const probs = new Float64Array( n );
204
+
205
+ for ( let i = 0; i < n; i += 1 ) {
206
+ const row = X[ i ];
207
+ let z = intercept;
208
+ for ( let j = 0; j < p; j += 1 ) {
209
+ z += coefficients[ j ] * row[ j ];
210
+ }
211
+ probs[ i ] = sigmoid( z );
212
+ }
213
+
214
+ return probs;
215
+ }; // predict()
216
+
217
+ // ── Classify ──────────────────────────────────────────────────────
218
+
219
+ /**
220
+ * Threshold probabilities into binary class labels.
221
+ *
222
+ * @param {Float64Array} probabilities — Output of predict().
223
+ * @param {number} [threshold=0.5]
224
+ * @returns {Uint8Array} Binary labels (0 or 1).
225
+ */
226
+ const classify = function ( probabilities, threshold ) {
227
+ const t = ( threshold === undefined ) ? 0.5 : threshold;
228
+ const n = probabilities.length;
229
+ const labels = new Uint8Array( n );
230
+
231
+ for ( let i = 0; i < n; i += 1 ) {
232
+ labels[ i ] = ( probabilities[ i ] >= t ) ? 1 : 0;
233
+ }
234
+
235
+ return labels;
236
+ }; // classify()
237
+
238
+ export { sigmoid, train, predict, classify };
@@ -0,0 +1,126 @@
1
+ // src/tools/training/metrics.js
2
+
3
+ /**
4
+ * @fileoverview Binary classification evaluation metrics.
5
+ *
6
+ * Provides confusion-matrix computation, standard derived metrics
7
+ * (precision, recall, F1, specificity, FPR, accuracy), and a threshold
8
+ * sweep utility for ROC-style analysis.
9
+ *
10
+ * All inputs are validated for binary values (exactly 0 or 1) to prevent
11
+ * silent miscounts.
12
+ */
13
+
14
+ import { validateBinaryArray } from './validate.js';
15
+
16
+ // ── Confusion Matrix ──────────────────────────────────────────────
17
+
18
+ /**
19
+ * Count true positives, false positives, false negatives, and true negatives.
20
+ *
21
+ * @param {ArrayLike<number>} predictions — Binary predictions (0 or 1).
22
+ * @param {ArrayLike<number>} labels — Ground truth labels (0 or 1).
23
+ * @returns {{ tp: number, fp: number, fn: number, tn: number }}
24
+ */
25
+ const confusionMatrix = function ( predictions, labels ) {
26
+ if ( predictions.length !== labels.length ) {
27
+ throw new Error( 'confusionMatrix: predictions and labels must have the same length.' );
28
+ }
29
+
30
+ validateBinaryArray( predictions, 'predictions', 'confusionMatrix' );
31
+ validateBinaryArray( labels, 'labels', 'confusionMatrix' );
32
+
33
+ const n = predictions.length;
34
+ let tp = 0;
35
+ let fp = 0;
36
+ let fn = 0;
37
+ let tn = 0;
38
+
39
+ for ( let i = 0; i < n; i += 1 ) {
40
+ const p = predictions[ i ];
41
+ const l = labels[ i ];
42
+ if ( p === 1 && l === 1 ) tp += 1;
43
+ else if ( p === 1 && l === 0 ) fp += 1;
44
+ else if ( p === 0 && l === 1 ) fn += 1;
45
+ else tn += 1;
46
+ }
47
+
48
+ return { tp, fp, fn, tn };
49
+ }; // confusionMatrix()
50
+
51
+ // ── Derived Metrics ───────────────────────────────────────────────
52
+
53
+ /**
54
+ * Derive standard classification metrics from a confusion matrix.
55
+ *
56
+ * @param {{ tp: number, fp: number, fn: number, tn: number }} cm
57
+ * @returns {{ precision: number, recall: number, f1: number, specificity: number, fpr: number, accuracy: number }}
58
+ */
59
+ const classificationMetrics = function ( cm ) {
60
+ const { tp, fp, fn, tn } = cm;
61
+ const total = tp + fp + fn + tn;
62
+
63
+ const precision = ( tp + fp ) > 0 ? tp / ( tp + fp ) : 0;
64
+ const recall = ( tp + fn ) > 0 ? tp / ( tp + fn ) : 0;
65
+ const f1 = ( precision + recall ) > 0 ?
66
+ ( 2 * precision * recall ) / ( precision + recall ) :
67
+ 0;
68
+ const specificity = ( tn + fp ) > 0 ? tn / ( tn + fp ) : 0;
69
+ const fpr = ( tn + fp ) > 0 ? fp / ( tn + fp ) : 0;
70
+ const accuracy = total > 0 ? ( tp + tn ) / total : 0;
71
+
72
+ return { precision, recall, f1, specificity, fpr, accuracy };
73
+ }; // classificationMetrics()
74
+
75
+ // ── Threshold Sweep ───────────────────────────────────────────────
76
+
77
+ /**
78
+ * Evaluate classification at multiple probability thresholds.
79
+ * Useful for ROC analysis and threshold selection.
80
+ *
81
+ * @param {Float64Array} probabilities — Predicted probabilities.
82
+ * @param {ArrayLike<number>} labels — Ground truth labels (0 or 1).
83
+ * @param {number[]} thresholds — Thresholds to evaluate.
84
+ * @returns {Array<{ threshold: number, tp: number, fp: number, fn: number, tn: number, precision: number, recall: number, f1: number, specificity: number, fpr: number }>}
85
+ */
86
+ const sweepThresholds = function ( probabilities, labels, thresholds ) {
87
+ if ( probabilities.length !== labels.length ) {
88
+ throw new Error( 'sweepThresholds: probabilities and labels must have the same length.' );
89
+ }
90
+
91
+ validateBinaryArray( labels, 'labels', 'sweepThresholds' );
92
+
93
+ const n = probabilities.length;
94
+ const results = new Array( thresholds.length );
95
+
96
+ for ( let t = 0; t < thresholds.length; t += 1 ) {
97
+ const thresh = thresholds[ t ];
98
+ let tp = 0;
99
+ let fp = 0;
100
+ let fn = 0;
101
+ let tn = 0;
102
+
103
+ for ( let i = 0; i < n; i += 1 ) {
104
+ const pred = ( probabilities[ i ] >= thresh ) ? 1 : 0;
105
+ const label = labels[ i ];
106
+ if ( pred === 1 && label === 1 ) tp += 1;
107
+ else if ( pred === 1 && label === 0 ) fp += 1;
108
+ else if ( pred === 0 && label === 1 ) fn += 1;
109
+ else tn += 1;
110
+ }
111
+
112
+ const precision = ( tp + fp ) > 0 ? tp / ( tp + fp ) : 0;
113
+ const recall = ( tp + fn ) > 0 ? tp / ( tp + fn ) : 0;
114
+ const f1 = ( precision + recall ) > 0 ?
115
+ ( 2 * precision * recall ) / ( precision + recall ) :
116
+ 0;
117
+ const specificity = ( tn + fp ) > 0 ? tn / ( tn + fp ) : 0;
118
+ const fpr = ( tn + fp ) > 0 ? fp / ( tn + fp ) : 0;
119
+
120
+ results[ t ] = { threshold: thresh, tp, fp, fn, tn, precision, recall, f1, specificity, fpr };
121
+ }
122
+
123
+ return results;
124
+ }; // sweepThresholds()
125
+
126
+ export { confusionMatrix, classificationMetrics, sweepThresholds };